Madmail

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

  1. 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
  2. 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
  3. 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.

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:

  1. Acesse Webhooks
  2. Clique no seu webhook
  3. 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
}