Webhooks
Visão geral
Os webhooks permitem que você receba requisições HTTP POST no seu servidor quando eventos acontecem no Madmail, como quando um e-mail é entregue, sofre bounce ou é clicado. Com isso você constrói integrações em tempo real e automatiza fluxos de trabalho.
Configurando webhooks
Crie um endpoint de webhook
Crie um endpoint no seu servidor capaz de receber requisições POST. O endpoint precisa:
- Aceitar requisições POST com corpo JSON
- Retornar um status 2xx para confirmar o recebimento
- Responder em até 10 segundos
Adicione o webhook no dashboard
Acesse Webhooks no seu dashboard do Madmail e crie um novo webhook:
- Informe a URL do seu endpoint
- Selecione quais eventos você quer receber
- Copie o secret de assinatura para a verificação
Verifique as assinaturas dos webhooks
Sempre verifique as assinaturas para garantir que as requisições vêm do Madmail. Veja a seção Verificação de assinatura abaixo.
Tipos de evento
Eventos de e-mail
| Evento | Descrição |
|---|---|
email.queued | O e-mail entrou na fila de envio |
email.sent | O e-mail foi enviado ao servidor de e-mail do destinatário |
email.delivered | O e-mail foi entregue com sucesso |
email.delivery_delayed | A entrega do e-mail está sendo retentada |
email.bounced | O e-mail sofreu bounce (permanente ou temporário) |
email.rejected | O e-mail foi rejeitado |
email.rendering_failure | Falha ao renderizar o template do e-mail |
email.complained | O destinatário marcou o e-mail como spam |
email.failed | Falha ao enviar o e-mail |
email.cancelled | Um e-mail agendado foi cancelado |
email.suppressed | O e-mail foi suprimido (destinatário na lista de supressão) |
email.opened | O destinatário abriu o e-mail |
email.clicked | O destinatário clicou em um link do e-mail |
Eventos de contato
| Evento | Descrição |
|---|---|
contact.created | Um novo contato foi criado |
contact.updated | O contato foi atualizado |
contact.deleted | O contato foi excluído |
Eventos de domínio
| Evento | Descrição |
|---|---|
domain.created | Um novo domínio foi adicionado |
domain.verified | A verificação do domínio foi concluída |
domain.updated | As configurações do domínio mudaram |
domain.deleted | O domínio foi excluído |
Payload do webhook
Cada requisição de webhook inclui um payload JSON com a estrutura abaixo. Veja Detalhes dos dados de evento para saber o conteúdo do campo data em cada tipo de evento.
{
"id": "call_abc123",
"type": "email.delivered",
"version": "2026-01-18",
"createdAt": "2024-01-15T10:30:00.000Z",
"teamId": 123,
"data": {
"id": "email_123",
"status": "DELIVERED",
"from": "sender@example.com",
"to": ["recipient@example.com"],
"subject": "Welcome!",
"occurredAt": "2024-01-15T10:30:00Z"
},
"attempt": 1
}
Campos do payload
| Campo | Descrição |
|---|---|
id | Identificador único desta chamada de webhook |
type | O tipo do evento (por exemplo, email.delivered) |
version | Versão da API para o formato do payload |
createdAt | Quando o evento foi criado |
teamId | O ID do seu time |
data | Dados específicos do evento (variam conforme o tipo) |
attempt | Número da tentativa de entrega (1-6) |
Cabeçalhos da requisição
Cada requisição de webhook inclui os seguintes cabeçalhos:
| Cabeçalho | Descrição |
|---|---|
X-UseSend-Signature | Assinatura HMAC-SHA256 para verificação |
X-UseSend-Timestamp | Timestamp Unix em milissegundos |
X-UseSend-Event | Tipo do evento |
X-UseSend-Call | ID único da chamada do webhook |
X-UseSend-Retry | true se for uma tentativa de reenvio |
Verificação de assinatura
Sempre verifique as assinaturas dos webhooks para garantir que as requisições são autênticas. A assinatura é calculada assim:
HMAC-SHA256(secret, "${timestamp}.${rawBody}")
Usando o SDK (recomendado)
npm install usesend-js
yarn add usesend-js
pnpm add usesend-js
bun add usesend-js
Next.js App Router
import { UseSend } from "usesend-js";
const usesend = new UseSend("us_your_api_key");
const webhooks = usesend.webhooks(process.env.USESEND_WEBHOOK_SECRET!);
export async function POST(request: Request) {
try {
const rawBody = await request.text();
const event = webhooks.constructEvent(rawBody, {
headers: request.headers,
});
switch (event.type) {
case "email.delivered":
console.log("Email delivered to:", event.data.to);
break;
case "email.bounced":
console.log("Email bounced:", event.data.id);
break;
case "email.opened":
console.log("Email opened:", event.data.id);
break;
}
return new Response("ok");
} catch (error) {
console.error("Webhook error:", error);
return new Response((error as Error).message, { status: 400 });
}
}
Express
import express from "express";
import { Webhooks } from "usesend-js";
const webhooks = new Webhooks(process.env.USESEND_WEBHOOK_SECRET!);
const app = express();
// Important: Use raw body parser for webhook routes
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = webhooks.constructEvent(req.body, {
headers: req.headers,
});
switch (event.type) {
case "email.delivered":
console.log("Email delivered to:", event.data.to);
break;
case "email.bounced":
console.log("Email bounced:", event.data.id);
break;
}
res.status(200).send("ok");
} catch (error) {
console.error("Webhook error:", error);
res.status(400).send((error as Error).message);
}
});
app.listen(3000);
Apenas verificação
Se você só precisa verificar a assinatura, sem fazer o parse do evento:
const isValid = webhooks.verify(rawBody, { headers: request.headers });
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
Verificação manual
Se você preferir verificar manualmente, sem o SDK:
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(
secret: string,
rawBody: string,
signature: string,
timestamp: string,
): boolean {
const expectedSignature = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const expected = Buffer.from(`v1=${expectedSignature}`, "utf8");
const received = Buffer.from(signature, "utf8");
if (expected.length !== received.length) {
return false;
}
return timingSafeEqual(expected, received);
}
// Usage
const signature = request.headers.get("X-UseSend-Signature");
const timestamp = request.headers.get("X-UseSend-Timestamp");
const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
Comportamento de retentativa
Se o seu endpoint não retornar uma resposta 2xx, o Madmail vai tentar entregar novamente com backoff exponencial:
| Tentativa | Intervalo |
|---|---|
| 1 | Imediato |
| 2 | ~5 segundos |
| 3 | ~10 segundos |
| 4 | ~20 segundos |
| 5 | ~40 segundos |
| 6 | ~80 segundos |
Depois de 6 tentativas sem sucesso, a chamada do webhook é marcada como falha.
Se o seu endpoint de webhook falhar em 30 chamadas consecutivas, o webhook será desativado automaticamente para evitar falhas contínuas. Você pode reativá-lo pelo dashboard.
Boas práticas
Responda rápido
Retorne uma resposta 2xx o mais rápido possível. Se precisar, processe os dados do webhook de forma assíncrona. As requisições expiram após 10 segundos.
Trate duplicidades
Use o campo id do payload para deduplicar eventos. Em casos raros, o mesmo
evento pode ser entregue mais de uma vez.
Verifique as assinaturas
Sempre verifique o cabeçalho X-UseSend-Signature para garantir que as
requisições vêm do Madmail e não foram adulteradas.
Confira os timestamps
Por padrão, o SDK rejeita assinaturas com mais de 5 minutos. Isso evita ataques de replay.
Use HTTPS
Sempre use endpoints HTTPS em produção para criptografar os dados do webhook em trânsito.
Testando webhooks
Você pode enviar um webhook de teste pelo dashboard para verificar se o seu endpoint está funcionando corretamente:
- Acesse Webhooks
- Clique no seu webhook
- Clique em "Send Test" para enviar um evento de teste
O evento de teste terá o tipo webhook.test com o seguinte payload:
{
"test": true,
"webhookId": "wh_abc123",
"sentAt": "2024-01-15T10:30:00.000Z"
}
Solução de problemas
O webhook não está recebendo eventos
- Confirme que a URL do endpoint está correta e acessível publicamente - Verifique se o endpoint retorna um status 2xx - Garanta que o webhook está com status ACTIVE no dashboard - Veja se o webhook foi desativado automaticamente por falhas consecutivas
A verificação de assinatura está falhando
- Use o corpo bruto da requisição, não o JSON já convertido - Confirme que
está usando o secret correto do webhook - Verifique se o timestamp não
expirou (janela de 5 minutos) - Confirme que está calculando o HMAC
corretamente:
HMAC-SHA256(secret, "${timestamp}.${rawBody}")
Webhook desativado automaticamente
Depois de 30 chamadas falhas consecutivas, os webhooks são desativados automaticamente. Corrija o problema no seu endpoint e reative o webhook pelo dashboard. O contador de falhas é zerado na próxima entrega bem-sucedida.
Detalhes dos dados de evento
Esta seção documenta a estrutura do campo data para cada tipo de evento.
Eventos de e-mail
A maioria dos eventos de e-mail compartilha uma estrutura base comum:
{
id: string; // Email ID
status: string; // Email status (e.g., "DELIVERED", "BOUNCED")
from: string; // Sender email address
to: string[]; // Recipient email addresses
occurredAt: string; // ISO 8601 timestamp
subject?: string; // Email subject
campaignId?: string; // Campaign ID (if from a campaign)
contactId?: string; // Contact ID (if sent to a contact)
domainId?: number; // Domain ID
templateId?: string; // Template ID (if using a template)
metadata?: object; // Custom metadata you attached to the email
}
email.bounced
Inclui detalhes adicionais do bounce:
{
// ... base email fields
bounce: {
type: "Transient" | "Permanent" | "Undetermined";
subType: "General" | "NoEmail" | "Suppressed" | "OnAccountSuppressionList"
| "MailboxFull" | "MessageTooLarge" | "ContentRejected" | "AttachmentRejected";
message?: string; // Bounce message from the mail server
}
}
email.failed
Inclui o motivo da falha:
{
// ... base email fields
failed: {
reason: string; // Failure reason
}
}
email.suppressed
Inclui detalhes da supressão:
{
// ... base email fields
suppression: {
type: "Bounce" | "Complaint" | "Manual";
reason: string; // Why the email was suppressed
source?: string; // Source of the suppression
}
}
email.opened
Inclui detalhes do rastreamento de abertura:
{
// ... base email fields
open: {
timestamp: string; // When the email was opened
userAgent?: string; // Browser/client user agent
ip?: string; // IP address
platform?: string; // Detected platform
}
}
email.clicked
Inclui detalhes do rastreamento de cliques:
{
// ... base email fields
click: {
timestamp: string; // When the link was clicked
url: string; // The clicked URL
userAgent?: string; // Browser/client user agent
ip?: string; // IP address
platform?: string; // Detected platform
}
}
Eventos de contato
Todos os eventos de contato (contact.created, contact.updated, contact.deleted) incluem:
{
id: string; // Contact ID
email: string; // Contact email address
contactBookId: string; // Contact book ID
subscribed: boolean; // Subscription status
properties: object; // Custom properties
firstName?: string; // First name
lastName?: string; // Last name
createdAt: string; // ISO 8601 timestamp
updatedAt: string; // ISO 8601 timestamp
}
Eventos de domínio
Todos os eventos de domínio (domain.created, domain.verified, domain.updated, domain.deleted) incluem:
{
id: number; // Domain ID
name: string; // Domain name (e.g., "example.com")
status: string; // Domain status
region: string; // AWS region
createdAt: string; // ISO 8601 timestamp
updatedAt: string; // ISO 8601 timestamp
clickTracking: boolean; // Click tracking enabled
openTracking: boolean; // Open tracking enabled
subdomain?: string; // Subdomain for tracking
dkimStatus?: string; // DKIM verification status
spfDetails?: string; // SPF record details
dmarcAdded?: boolean; // DMARC record added
}