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.
https://api.b20mail.com| Item | Valor |
|---|---|
| Protocolo | HTTPS (HTTP aceito em dev local) |
| Formato | JSON (Content-Type: application/json) |
| Autenticação | API Key via header x-api-key |
| Provider | Amazon SES via AWS SDK PHP |
| Logs | MySQL + 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.
Crie todos os registros no DNS do seu domínio (Cloudflare, Registro.br, etc.):
| Para quê | Tipo | Qtd |
|---|---|---|
| DKIM — assina seus e-mails (libera o envio) | CNAME | 3 |
| DMARC — política de monitoramento | TXT | 1 |
MAIL FROM — retorno de bounce (em mail.seudominio) | MX | 1 |
| MAIL FROM — SPF alinhado | TXT | 1 |
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.
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.
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>
- 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 .env | Padrão | Descrição |
|---|---|---|
| RATE_LIMIT_PER_MINUTE | 60 | Má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
# Resposta 200 { "success": true, "message": "pong", "version": "1.0.0", "env": "local", "time": "2026-05-18 16:00:00" }
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âmetro | Tipo | Obrig. | Descrição |
|---|---|---|---|
| to | string | Sim | Email do destinatário. |
| from | string | Sim | Remetente ("Nome <email@dominio.com>"). Só funciona se o domínio foi cadastrado e validado no seu painel. |
| subject | string | Sim | Assunto do email (max 200 chars) |
| template | string | Sim* | Nome do template: welcome, recover-password, notification. *Obrigatório se não enviar html |
| html | string | Não | Envie seu próprio HTML puro aqui. A API aceita o HTML completo e ainda injeta suas variáveis nele. Sobrescreve o template. |
| type | string | Não | transactional ou marketing (padrão: transactional). Valores diferentes retornam 422. |
| variables | object | Não | Variáveis para renderizar no template ou no seu html customizado. |
| headers | object | Não | Cabeç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.
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.
"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çalho | RFC | O que faz |
|---|---|---|
List-Unsubscribe | 2369 | Uma URL e/ou um mailto: de descadastro, cada um entre < > e separados por vírgula. É o destino que o provedor usa. |
List-Unsubscribe-Post | 8058 | Autoriza o um clique. Valor sempre literal: List-Unsubscribe=One-Click. |
Como funciona na prática
- Você envia os dois cabeçalhos no campo
headers. - O Gmail/Yahoo desenha o link "Cancelar inscrição" ao lado do seu nome — a interface dele, não a sua.
- A pessoa aperta. O provedor faz um
POSTpara a sua URL, com o corpo literalList-Unsubscribe=One-Click. - 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 você faz do seu lado
- Uma URL que aceite o POST do provedor — ela recebe o corpo
List-Unsubscribe=One-Clicke descadastra o contato. - Essa rota NÃO pode exigir CSRF/sessão. O Gmail posta sem cookie e sem token de sessão; se a rota exigir, o botão quebra. Proteja com um token aleatório na própria URL (ex.: 32+ caracteres, um por destinatário) e aceite só o corpo exato
List-Unsubscribe=One-Click(com throttle e log). - Não reaproveite a rota do formulário da sua tela: uma rota sem CSRF que também descadastra por GET/censo do corpo viraria um POST sem proteção nenhuma.
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.
Query Parameters
| Parâmetro | Tipo | Obrig. | Descrição |
|---|---|---|---|
| limit | int | Não | Quantidade de registros (Padrão: 50. Máx: 100). |
| offset | int | Não | Paginação (Padrão: 0). |
| status | string | Não | Filtrar 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 ↓.
Query Parameters
| Parâmetro | Tipo | Obrig. | Descrição |
|---|---|---|---|
| period | string | Não | Perí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étrica | Como é medida | Confiança |
|---|---|---|
Aberturaopened_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. |
Cliqueclicked_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. |
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:
- Links de descadastro (List-Unsubscribe) — é rota de conformidade, não ganha hop extra;
mailto:,tel:e âncoras internas (#).
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
| Template | Uso | Variáveis principais |
|---|---|---|
| welcome | Boas-vindas ao novo usuário | name, link |
| recover-password | Recuperação de senha | name, link |
| notification | Notificação genérica | title, message, name, link (opcional) |
| default | Layout genérico (fallback automático) | subject, title, message, link, button_text, company_name |
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.
{{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ção | Como fazer |
|---|---|
| Criar nova key | O admin cria o projeto e gera a chave automaticamente via dashboard. |
| Ver keys ativas | Aba API KEYS no dashboard — mostra label, plano, status e métricas. |
| Ativar/Desativar | Botão de toggle na tabela de keys (apenas superadmin). |
| Quickstart | Ao selecionar uma key, o painel exibe código pronto em Node.js, PHP e cURL com a chave injetada. |
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
| Evento | Trigger | Descrição |
|---|---|---|
| email.bounced | AWS SNS Bounce | E-mail rejeitado pelo servidor destino |
| email.complained | AWS SNS Complaint | Destinatário marcou como spam |
| webhook.test | Painel Admin | Evento 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:
| Header | Descrição |
|---|---|
| X-B20Mail-Event | Tipo do evento (email.bounced, etc.) |
| X-B20Mail-Timestamp | Unix timestamp do envio |
| X-B20Mail-Signature | Assinatura sha256=HMAC(timestamp.payload, secret) |
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
- Acesse o Dashboard e clique na aba
WEBHOOKSna barra lateral - Clique em "+ NOVO ENDPOINT"
- Selecione o projeto (API Key) vinculado
- Informe a URL HTTPS do seu endpoint receptor
- Marque os eventos que deseja receber (
email.bounced,email.complained) - Clique em "CRIAR ENDPOINT"
- Copie o secret HMAC exibido — ele será mostrado apenas uma vez
B20MAIL_WEBHOOK_SECRET). Se perdido, remova o endpoint e crie um novo para gerar um novo secret.CÓDIGOS DE ERRO
| HTTP | Significado | Causa comum |
|---|---|---|
| 400 | Bad Request | JSON malformado no body |
| 401 | Unauthorized | Header x-api-key ausente ou inválido |
| 404 | Not Found | Rota inexistente (confira a URL) |
| 405 | Method Not Allowed | Método HTTP errado para a rota. O header Allow da resposta informa quais são aceitos — /send-email só aceita POST. |
| 422 | Unprocessable | Email inválido, template inexistente, campo ausente |
| 429 | Too Many Requests | Rate limit excedido para esta chave |
| 500 | Server Error | Falha na AWS SES ou banco de dados |
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.