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

EventoDescrição
email.queuedO e-mail entrou na fila de envio
email.sentO e-mail foi enviado ao servidor de e-mail do destinatário
email.deliveredO e-mail foi entregue com sucesso
email.delivery_delayedA entrega do e-mail está sendo retentada
email.bouncedO e-mail sofreu bounce (permanente ou temporário)
email.rejectedO e-mail foi rejeitado
email.rendering_failureFalha ao renderizar o template do e-mail
email.complainedO destinatário marcou o e-mail como spam
email.failedFalha ao enviar o e-mail
email.cancelledUm e-mail agendado foi cancelado
email.suppressedO e-mail foi suprimido (destinatário na lista de supressão)
email.openedO destinatário abriu o e-mail
email.clickedO destinatário clicou em um link do e-mail

Eventos de contato

EventoDescrição
contact.createdUm novo contato foi criado
contact.updatedO contato foi atualizado
contact.deletedO contato foi excluído

Eventos de domínio

EventoDescrição
domain.createdUm novo domínio foi adicionado
domain.verifiedA verificação do domínio foi concluída
domain.updatedAs configurações do domínio mudaram
domain.deletedO 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

CampoDescrição
idIdentificador único desta chamada de webhook
typeO tipo do evento (por exemplo, email.delivered)
versionVersão da API para o formato do payload
createdAtQuando o evento foi criado
teamIdO ID do seu time
dataDados específicos do evento (variam conforme o tipo)
attemptNúmero da tentativa de entrega (1-6)

Cabeçalhos da requisição

Cada requisição de webhook inclui os seguintes cabeçalhos:

CabeçalhoDescrição
X-UseSend-SignatureAssinatura HMAC-SHA256 para verificação
X-UseSend-TimestampTimestamp Unix em milissegundos
X-UseSend-EventTipo do evento
X-UseSend-CallID único da chamada do webhook
X-UseSend-Retrytrue 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

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:

TentativaIntervalo
1Imediato
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.

Atenção

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:

  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
}