Documentação API

Referência completa para integrar qualquer sistema interno da B20 à B20Mail. A API é REST, aceita JSON e autentica por API Key via header.

VISÃO GERAL

A B20Mail é o único ponto de acesso ao Amazon SES na B20. Nenhum sistema deve configurar credenciais AWS diretamente — todos os envios passam por esta API.

Endpoint de Produção Oficial: https://api.b20mail.com
ItemValor
ProtocoloHTTPS (HTTP aceito em dev local)
FormatoJSON (Content-Type: application/json)
AutenticaçãoAPI Key via header x-api-key
ProviderAmazon SES via AWS SDK PHP
LogsMySQL + arquivo mensal em logs/

VERIFICAR SEU DOMÍNIO

O B20Mail só envia por domínios verificados — é isso que protege a sua reputação e impede envios em seu nome. Você não precisa mexer na AWS: o painel gera os registros de DNS, e a captura de bounce é automática do nosso lado.

Onde: painel → aba REMETENTES → Novo domínio. O modal mostra os registros abaixo.

Crie todos os registros no DNS do seu domínio (Cloudflare, Registro.br, etc.):

Para quêTipoQtd
DKIM — assina seus e-mails (libera o envio)CNAME3
DMARC — política de monitoramentoTXT1
MAIL FROM — retorno de bounce (em mail.seudominio)MX1
MAIL FROM — SPF alinhadoTXT1
Cuidados: copie os valores exatamente como aparecem (sem espaços extras); no Cloudflare deixe esses registros como "DNS only" (nuvem cinza); o subdomínio mail.seudominio não precisa de caixa de e-mail — o MX só roteia o retorno técnico do bounce.

Depois de criar, aguarde a propagação do DNS (de minutos a algumas horas) e clique Verificar no painel. Quando o DKIM ficar verificado, você já pode enviar.

Ordem de importância: o DKIM é o que libera o envio. O MAIL FROM melhora a entregabilidade (SPF alinhado) e pode levar um pouco mais para verificar — sem travar o envio.

Com o domínio pronto, os bounces, aberturas e entregas já aparecem no painel e em GET /logs — sem precisar de webhook. O webhook é opcional, só para receber os eventos por push.

📩 Cópia de bounce por e-mail — desligada por padrão. A AWS pode mandar uma cópia de cada bounce/complaint por e-mail para o seu remetente. Como a B20Mail já registra tudo (nos Relatórios, no GET /logs e no webhook), essa cópia é redundante e vem desligada — sem poluir sua caixa. Se você preferir receber a cópia por e-mail, tem um botão "Cópia de bounce por e-mail" em REMETENTES → (seu domínio) → DNS para ligar. Desligar não afeta o registro de bounces em lugar nenhum.

AUTENTICAÇÃO

Toda requisição às rotas protegidas deve incluir o header x-api-key com a chave do projeto. Sua chave fica no painel, na aba API Keys — é uma sequência de 64 caracteres hexadecimais.

# Header obrigatório em todas as requisições autenticadas
x-api-key: <cole aqui a chave de 64 caracteres do seu projeto>
Segurança da sua API Key — leia antes de integrar. A chave dá poder total de envio em nome dos seus domínios verificados. Trate-a como uma senha de produção:
  • Somente no servidor (server-to-server). Use a chave no backend do seu sistema. Nunca em JavaScript de navegador, app mobile ou qualquer código que chegue ao usuário final — a API recusa chamadas de navegador (CORS) justamente para impedir esse tipo de vazamento.
  • Sempre via variável de ambiente. Carregue a chave de .env / variável de ambiente. Nunca escreva a chave direto no código-fonte nem faça commit dela em repositório (mesmo privado).
  • Nunca em logs, prints ou tickets. Ao pedir suporte ou compartilhar uma tela, oculte a chave.
  • Rotacione se vazar. Se suspeitar de exposição, desative o projeto no painel e gere uma nova chave — a antiga deixa de funcionar imediatamente.

RATE LIMITING

Cada API Key possui um limite de requisições por minuto configurado no .env (padrão: 60 req/min). Ao exceder, a API retorna 429 Too Many Requests.

Variável .envPadrãoDescrição
RATE_LIMIT_PER_MINUTE60Máximo de requisições por minuto por chave

FORMATO DE RESPOSTA

Todas as respostas seguem o mesmo envelope JSON:

{
  "success":    true | false,
  "message":    "Mensagem descritiva",
  "request_id": "uuid-para-rastreio",
  // campos extras dependendo da rota
}

ENDPOINTS

GETPÚBLICO/ping
Verifica a saúde da API e a latência do servidor. Não requer autenticação. Use para verificar se o serviço está online.
# Resposta 200
{
  "success":    true,
  "message":    "pong",
  "version":    "1.0.0",
  "env":        "local",
  "time":       "2026-05-18 16:00:00"
}
POST/send-email
Envia um email usando um template pré-definido. Requer API Key válida.
⚠️ REMETENTE OBRIGATÓRIO E AUTORIZADO: A B20Mail opera como uma infraestrutura pura de envios White-Label. Isso significa que não assumimos a autoria dos seus disparos. Você é obrigado a assinar seus e-mails preenchendo a chave from no formato Nome <email@dominio.com>. Além disso, o seu domínio precisa estar obrigatoriamente cadastrado e verificado na aba "REMETENTES" do seu painel.

Body da Requisição

ParâmetroTipoObrig.Descrição
tostringSimEmail do destinatário.
fromstringSimRemetente ("Nome <email@dominio.com>"). Só funciona se o domínio foi cadastrado e validado no seu painel.
subjectstringSimAssunto do email (max 200 chars)
templatestringSim*Nome do template: welcome, recover-password, notification.
*Obrigatório se não enviar html
htmlstringNãoEnvie seu próprio HTML puro aqui. A API aceita o HTML completo e ainda injeta suas variáveis nele. Sobrescreve o template.
typestringNãotransactional ou marketing (padrão: transactional). Valores diferentes retornam 422.
variablesobjectNãoVariáveis para renderizar no template ou no seu html customizado.
headersobjectNãoCabeçalhos extras permitidos (whitelist). Hoje aceitos: List-Unsubscribe e List-Unsubscribe-Post — o descadastro de um clique do Gmail/Yahoo. Qualquer outro cabeçalho é ignorado por segurança. Ver seção dedicada ↓
# Exemplo completo
POST /send-email
Content-Type: application/json

{
  "from":      "Sua Empresa <suporte@empresa.com>",
  "to":        "joao@empresa.com",
  "subject":   "Bem-vindo à B20!",
  "template":  "welcome",
  "type":      "transactional",
  "variables": {
    "name": "João Silva",
    "link": "https://seusistema.com/comecar"
  }
}

// Response 200 — o e-mail entra na FILA e é enviado pelo worker em segundos.
{
  "success":    true,
  "message":    "Email enfileirado com sucesso",
  "request_id": "a1b2c3-...",
  "message_id": null
}
// message_id fica null na resposta (o envio é assíncrono). Consulte o
// status final e o message_id da AWS depois via GET /logs.
HEADERList-Unsubscribe — descadastro de um clique
O botão "Cancelar inscrição" que o Gmail e o Yahoo desenham no topo do e-mail. Para campanhas (type: marketing).

Por que isso importa

Desde 2024, Gmail e Yahoo exigem o descadastro de um clique de quem envia em volume (a partir de ~5.000 mensagens/dia para o mesmo provedor), junto com SPF/DKIM/DMARC e taxa de spam abaixo de 0,3%.

Mas o benefício maior nem é a regra: quem não acha o botão de descadastrar aperta "Spam". E reclamação de spam pesa muito mais que um descadastro — é ela que derruba a entrega de todos os seus e-mails, inclusive os transacionais. Oferecer a saída protege a sua reputação.

⚠️ Declarar campanha sem oferecer saída é pior que não declarar. Se você manda "type": "marketing" mas não inclui o List-Unsubscribe, o provedor te mede pela régua mais dura e não encontra o descadastro. Marketing e o cabeçalho andam juntos.

Os dois cabeçalhos

CabeçalhoRFCO que faz
List-Unsubscribe2369Uma URL e/ou um mailto: de descadastro, cada um entre < > e separados por vírgula. É o destino que o provedor usa.
List-Unsubscribe-Post8058Autoriza o um clique. Valor sempre literal: List-Unsubscribe=One-Click.

Como funciona na prática

  1. Você envia os dois cabeçalhos no campo headers.
  2. O Gmail/Yahoo desenha o link "Cancelar inscrição" ao lado do seu nome — a interface dele, não a sua.
  3. A pessoa aperta. O provedor faz um POST para a sua URL, com o corpo literal List-Unsubscribe=One-Click.
  4. Fim. A pessoa não sai do Gmail, não vê tela sua, não confirma nada. Você recebe o POST e remove o contato.
# Exemplo — campanha com descadastro de um clique
POST /send-email
Content-Type: application/json

{
  "from":    "Sua Empresa <news@empresa.com>",
  "to":      "pessoa@gmail.com",
  "subject": "Novidades da semana",
  "html":    "<html>...</html>",
  "type":    "marketing",
  "headers": {
    "List-Unsubscribe":      "<https://empresa.com/descadastro/TOKEN>, <mailto:sair@empresa.com>",
    "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
  }
}
O que a B20Mail já faz por você: ao receber esses cabeçalhos, nós os incluímos na mensagem e o DKIM da AWS os assina automaticamente (o RFC 8058 exige a assinatura). Você não configura nada de DKIM.

O que você faz do seu lado

Segurança do campo headers: é uma whitelist — só List-Unsubscribe e List-Unsubscribe-Post passam. Qualquer outro cabeçalho (Bcc, Reply-To, etc.) é descartado, e quebras de linha nos valores são removidas. Isso impede injeção de cabeçalho pela API.
GET/logs
Retorna o histórico bruto de e-mails processados. Ideal para rotinas diárias de backup do lado do cliente (via Cron Job). Máximo de 100 registros por chamada.

Query Parameters

ParâmetroTipoObrig.Descrição
limitintNãoQuantidade de registros (Padrão: 50. Máx: 100).
offsetintNãoPaginação (Padrão: 0).
statusstringNãoFiltrar por status: sent, failed, queued.
# Exemplo de Requisição (cURL)
curl -X GET "https://api.b20mail.com/logs?limit=100&status=failed" \
     -H "x-api-key: SUA_CHAVE_AQUI"

// Response 200
{
  "success": true,
  "message": "Logs recuperados",
  "request_id": "xyz-...",
  "count": 100,
  "limit": 100,
  "offset": 0,
  "logs": [
    {
      "request_id": "abc-...",
      "recipient": "cliente@email.com",
      "status": "sent",
      "error_message": null,
      "created_at": "2026-06-15 14:00:00",
      "opened_at": "2026-06-15 14:07:10",   // null se nunca aberto (pixel)
      "clicked_at": "2026-06-15 14:08:02"    // null se nunca clicou — métrica humana
    }
  ]
}

Use opened_at e clicked_at para auditar destinatário a destinatário: escolha um e-mail que você conheça e confira quando saiu, quando abriu e quando clicou. Sobre a diferença entre os dois, veja Aberturas e Cliques ↓.

GET/stats
Retorna a matemática agregada e a timeline de envios do seu projeto. Perfeito para construir e renderizar seus próprios painéis analíticos.

Query Parameters

ParâmetroTipoObrig.Descrição
periodstringNãoPeríodo analisado: 24h, 7d, 30d, 90d (Padrão: 30d).
# Exemplo de Requisição
GET /stats?period=7d

// Response 200
{
  "success": true,
  "message": "Stats OK",
  "request_id": "xyz-...",
  "period": "7d",
  "overview": {
    "total": 1462,
    "sent": 1450,
    "failed": 12,
    "opened": 840,   // aberturas por pixel (superestima — ver ↓)
    "clicked": 312   // cliques em links — ação humana, não infla
  },
  "timeline": [
    { "label": "2026-06-12", "total": 502, "sent": 500, "failed": 2, "opened": 280, "transactional": 300, "marketing": 200 },
    { "label": "2026-06-13", "total": 960, "sent": 950, "failed": 10, "opened": 560, "transactional": 650, "marketing": 300 }
  ]
}

ABERTURAS E CLIQUES — COMO LER (E DEFENDER) OS NÚMEROS

Toda ferramenta de e-mail do mundo mede engajamento de dois jeitos. Entender a diferença é o que te permite apresentar os números com segurança — inclusive para um cliente cético.

MétricaComo é medidaConfiança
Abertura
opened_at
Uma imagem invisível de 1×1 (pixel) é embutida no e-mail. Quando o programa de e-mail carrega essa imagem, marcamos "aberto". Superestima. Apps que pré-carregam imagens — com destaque para o Apple Mail Privacy Protection (iPhone/Mac, desde 2021) — abrem o pixel automaticamente, antes de a pessoa ler. Boa parte das "aberturas" de qualquer remetente é isso. Serve de tendência, não de prova.
Clique
clicked_at
Reescrevemos os links do corpo para passar por /r, que registra o clique e redireciona ao destino real (transparente para o leitor). Alta — não infla. Um clique exige um dedo humano no link; nenhum app de e-mail clica sozinho. É a métrica que se sustenta em qualquer contestação.
Por que a abertura costuma vir "alta demais": não é erro nem invenção do relatório — é o pixel sendo aberto por robôs de privacidade (Apple, antivírus corporativo, proxies). Todo provedor sério (Mailchimp, RD Station, SendGrid) convive com o mesmo teto frouxo. Por isso, ao apresentar resultados, lidere pelo clique e pela conversão, não pela abertura.

O rastreio de clique é automático

Não há nada a configurar: todo e-mail com link http(s) no corpo já sai com os links rastreados. Exceções propositais, que não são reescritas:

Segurança: o link rastreado é assinado (HMAC). O /r só redireciona se a assinatura bater com o segredo do servidor — ninguém consegue forjar um link seu apontando para um site de phishing (não somos um "open redirect").

A prova que encerra qualquer dúvida: cruzar com o SEU sistema

Abertura e clique são nossos. A prova definitiva é sua: dos convidados, quantos viraram cadastro/login no seu sistema? Esse número está no seu banco de dados. Cruzando os envios (auditáveis em GET /logs, destinatário a destinatário) com as conversões reais, fecha-se a conta de ponta a ponta — sem depender de confiar em pixel nenhum.

TEMPLATES DISPONÍVEIS

TemplateUsoVariáveis principais
welcomeBoas-vindas ao novo usuárioname, link
recover-passwordRecuperação de senhaname, link
notificationNotificação genéricatitle, message, name, link (opcional)
defaultLayout genérico (fallback automático)subject, title, message, link, button_text, company_name
Importante: use exatamente estes nomes de variáveis (em inglês) dentro do objeto variables da requisição. Nomes diferentes não serão substituídos e o e-mail sairá com o campo em branco.

ENVIANDO SEU PRÓPRIO HTML (CUSTOM)

Se você não quiser usar os templates da B20Mail, você pode desenhar e enviar o seu próprio código HTML cru. Basta omitir a variável template e preencher a variável html na requisição JSON.

Mágica das Variáveis: Mesmo enviando seu próprio HTML cru, você ainda pode usar {{sua_var}} dentro dele e enviar no objeto variables. A B20Mail vai procurar e substituir as variáveis automaticamente antes de disparar.

SINTAXE DE VARIÁVEIS

Use {{variavel}} nos templates. As variáveis são sanitizadas automaticamente contra XSS.

<!-- No template HTML -->
<p>Olá, {{name}}!</p>
<a href="{{link}}">Recuperar senha</a>

TUTORIAL: CLASSE DE INTEGRAÇÃO COMPLETA (PHP)

Abaixo apresentamos um exemplo de classe Mailer.php pronta para uso em produção, ideal para projetos PHP (puros ou frameworks). Ela demonstra como ler as configurações do .env, processar o envio inteligente (priorizando arquivos HTML locais com fallback para a B20Mail) e tratar retornos e logs de erro.

1. Configuração do .env

# Arquivo .env do seu projeto
B20MAIL_API_URL="https://api.b20mail.com"
B20MAIL_API_KEY="SUA_CHAVE_AQUI"
MAIL_FROM_ADDRESS="Sua Empresa <suporte@seudominio.com>"

2. Classe Mailer.php

<?php
/**
 * Classe de Integração SaaS — B20Mail
 */

class Mailer {
    /**
     * Dispara um e-mail via B20Mail API
     */
    public static function send(string $to, string $subject, array $variables = [], string $templateName = 'default', string $type = 'transactional'): bool {
        $apiKey = trim($_ENV['B20MAIL_API_KEY'] ?? getenv('B20MAIL_API_KEY') ?? '', '"\'');
        
        if (empty($apiKey)) {
            error_log('B20MAIL_API_KEY não configurada');
            return false;
        }

        $mailFrom = trim($_ENV['MAIL_FROM_ADDRESS'] ?? getenv('MAIL_FROM_ADDRESS') ?? '', '"\'');
        
        $payload = [
            'from'      => $mailFrom ? $mailFrom : 'Sua Empresa <suporte@seudominio.com>',
            'to'        => $to,
            'subject'   => $subject,
            'type'      => $type,
            'variables' => array_merge($variables, ['email' => $to])
        ];

        // Tenta carregar o template HTML da pasta local. Se não achar, delega pro template global da B20Mail.
        $templateFile = __DIR__ . '/views/emails/' . $templateName . '.html';
        if (file_exists($templateFile)) {
            $payload['html'] = file_get_contents($templateFile);
        } else {
            $payload['template'] = $templateName;
        }

        $apiUrl = trim($_ENV['B20MAIL_API_URL'] ?? getenv('B20MAIL_API_URL') ?? '', '"\'');
        $apiUrl = rtrim($apiUrl ?: 'https://api.b20mail.com', '/');
        
        $ch = curl_init($apiUrl . '/send-email');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => [
                'x-api-key: ' . $apiKey,
                'Content-Type: application/json'
            ],
            CURLOPT_POSTFIELDS     => json_encode($payload)
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 200 && $httpCode < 300) {
            return true;
        }

        error_log('B20Mail Erro (HTTP ' . $httpCode . '): ' . $response);
        return false;
    }
}

EXEMPLOS DE INTEGRAÇÃO BÁSICA

PHP / cURL

<?php
$ch = curl_init('https://api.b20mail.com/send-email');
$payload = json_encode([
    'from'      => 'Sua Empresa <suporte@seudominio.com>',
    'to'        => 'user@email.com',
    'subject'   => 'Bem-vindo!',
    'template'  => 'welcome',
    'type'      => 'transactional',
    'variables' => ['name' => 'João', 'link' => 'https://seusistema.com/comecar'],
]);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        'x-api-key: SUA_CHAVE_AQUI',
        'Content-Type: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

JavaScript / Fetch

async function sendEmail() {
    const response = await fetch('https://api.b20mail.com/send-email', {
      method: 'POST',
      headers: {
        'x-api-key':    'SUA_CHAVE_AQUI',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from:      'Sua Empresa <suporte@seudominio.com>',
        to:        'user@email.com',
        subject:   'Bem-vindo!',
        template:  'welcome',
        type:      'transactional',
        variables: { name: 'João', link: 'https://seusistema.com/comecar' },
      }),
    });
    const data = await response.json();
}

Python / requests

import requests

payload = {
    "from":      "Sua Empresa <suporte@seudominio.com>",
    "to":        "user@email.com",
    "subject":   "Bem-vindo!",
    "template":  "welcome",
    "type":      "transactional",
    "variables": {"name": "João", "link": "https://seusistema.com/comecar"},
}

response = requests.post(
    "https://api.b20mail.com/send-email",
    headers={
        "x-api-key":    "SUA_CHAVE_AQUI",
        "Content-Type": "application/json",
    },
    json=payload,
)
data = response.json()

GERAR API KEY

As API Keys são gerenciadas diretamente pelo Painel Administrativo. Acesse a aba API KEYS no dashboard para:

AçãoComo fazer
Criar nova keyO admin cria o projeto e gera a chave automaticamente via dashboard.
Ver keys ativasAba API KEYS no dashboard — mostra label, plano, status e métricas.
Ativar/DesativarBotão de toggle na tabela de keys (apenas superadmin).
QuickstartAo selecionar uma key, o painel exibe código pronto em Node.js, PHP e cURL com a chave injetada.
Atenção: Trate sua API Key como uma senha. Nunca exponha em repositórios públicos ou código frontend.

WEBHOOKS

A B20Mail pode notificar seu sistema em tempo real quando um e-mail sofre bounce (rejeição) ou complaint (spam). Basta cadastrar um endpoint HTTPS no painel ou via API.

Eventos Disponíveis

EventoTriggerDescrição
email.bouncedAWS SNS BounceE-mail rejeitado pelo servidor destino
email.complainedAWS SNS ComplaintDestinatário marcou como spam
webhook.testPainel AdminEvento de teste para validar conectividade

Formato do Payload

Todos os webhooks são enviados como POST com body JSON:

{
  "event":     "email.bounced",
  "timestamp": "2026-06-29T21:00:00Z",
  "data": {
    "message_id":     "0100018ff123abc-abcd-1234",
    "request_id":     "a1b2c3d4e5f6-123456",
    "recipient":      "user@email.com",
    "subject":        "Bem-vindo!",
    "type":           "Permanent",
    "diagnostic":     "Bounce: 550 5.1.1 User unknown",
    "original_event": "Bounce"
  }
}

Verificação de Assinatura (HMAC-SHA256)

Cada webhook inclui headers de assinatura para garantir autenticidade:

HeaderDescrição
X-B20Mail-EventTipo do evento (email.bounced, etc.)
X-B20Mail-TimestampUnix timestamp do envio
X-B20Mail-SignatureAssinatura sha256=HMAC(timestamp.payload, secret)
Segurança: Sempre valide a assinatura antes de processar o evento. Rejeite requisições com timestamp com mais de 5 minutos de diferença. A assinatura cobre o corpo BRUTO da requisição — valide sobre o body cru (não re-serialize o JSON, senão a assinatura nunca vai bater).

Exemplos de Handler

PHP

<?php
$secret    = 'whsec_seu_secret_aqui';
$payload   = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_B20MAIL_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_B20MAIL_SIGNATURE'] ?? '';

// Validar assinatura
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    die('Assinatura inválida');
}

$data  = json_decode($payload, true);
$event = $data['event'] ?? '';

match($event) {
    'email.bounced'    => handleBounce($data['data']),
    'email.complained' => handleComplaint($data['data']),
    default            => null,
};

http_response_code(200);
echo 'OK';

Node.js / Express

const crypto = require('crypto');

app.post('/webhook/b20mail', express.raw({ type: 'application/json' }), (req, res) => {
    const secret    = process.env.B20MAIL_WEBHOOK_SECRET;
    const timestamp = req.headers['x-b20mail-timestamp'];
    const signature = req.headers['x-b20mail-signature'];
    // corpo BRUTO — NÃO use JSON.stringify(req.body): quebra a assinatura
    const payload   = req.body.toString('utf8');

    const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(timestamp + '.' + payload)
        .digest('hex');

    if (expected !== signature) return res.status(401).send('Invalid');

    const { event, data } = JSON.parse(payload);
    console.log(`Evento: ${event}`, data);
    res.status(200).send('OK');
});

Python / Flask

import hmac, hashlib, json
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook/b20mail', methods=['POST'])
def webhook():
    secret    = "whsec_seu_secret".encode()
    payload   = request.get_data(as_text=True)
    timestamp = request.headers.get('X-B20Mail-Timestamp', '')
    signature = request.headers.get('X-B20Mail-Signature', '')

    expected = 'sha256=' + hmac.new(
        secret, (timestamp + '.' + payload).encode(), hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return 'Invalid', 401

    data = json.loads(payload)
    print(f"Evento: {data['event']}", data['data'])
    return 'OK', 200

Como Cadastrar seu Endpoint

Passo a passo:
  1. Acesse o Dashboard e clique na aba WEBHOOKS na barra lateral
  2. Clique em "+ NOVO ENDPOINT"
  3. Selecione o projeto (API Key) vinculado
  4. Informe a URL HTTPS do seu endpoint receptor
  5. Marque os eventos que deseja receber (email.bounced, email.complained)
  6. Clique em "CRIAR ENDPOINT"
  7. Copie o secret HMAC exibido — ele será mostrado apenas uma vez
Segurança: Armazene o secret em uma variável de ambiente (B20MAIL_WEBHOOK_SECRET). Se perdido, remova o endpoint e crie um novo para gerar um novo secret.

CÓDIGOS DE ERRO

HTTPSignificadoCausa comum
400Bad RequestJSON malformado no body
401UnauthorizedHeader x-api-key ausente ou inválido
404Not FoundRota inexistente (confira a URL)
405Method Not AllowedMétodo HTTP errado para a rota. O header Allow da resposta informa quais são aceitos — /send-email só aceita POST.
422UnprocessableEmail inválido, template inexistente, campo ausente
429Too Many RequestsRate limit excedido para esta chave
500Server ErrorFalha na AWS SES ou banco de dados
Validando o endpoint na sua ferramenta: algumas plataformas de integração testam a URL com GET antes de usá-la. Como /send-email aceita apenas POST, elas recebem 405 — o que significa "a rota existe, use outro método", e não que o endpoint esteja fora do ar. Para um teste de disponibilidade, use GET /ping, que é público e responde 200.