Documentação
Guias passo a passo, troubleshooting e exemplos práticos. Pra detalhes de feature use as páginas /recursos/*. Pra referência completa da API com 155 endpoints, veja /api.
Introdução
RaviMail é uma plataforma SaaS de email marketing com infraestrutura própria (SMTP, IPs, warmup automático, DKIM/SPF/DMARC), CRM kanban, sequências automatizadas, IA classificando respostas e integração WhatsApp. Tudo no mesmo painel, sem add-ons separados.
Você pode usar 3 caminhos pra disparar emails:
- Painel web: criar campanhas, editar templates, ver dashboard ao vivo
- API REST em
api.ravimail.com.br/v1com 155 endpoints documentados - SMTP Relay em
smtp.ravimail.com.br:587com o token Bearer como senha
Todos os planos têm acesso aos 3 caminhos. Sandbox grátis com token rvm_test_* pra desenvolver sem queimar crédito.
Criar conta e verificar domínio
Cadastro grátis em ravimail.com.br/register (nome, email, senha). Você recebe um email de confirmação e cai no wizard de setup inicial.
Adicionar domínio remetente
- 1 No painel, Domínios → Adicionar domínio. Digite só o domínio (ex:
meudominio.com.br, semhttps://). - 2 Escolha: "Configurar no meu DNS atual" (recomendado, manter seu DNS) ou "Usar Ravi DNS" (delega tudo pra gente, exige trocar NS no registro.br).
- 3 Pague R$ 97 via PIX (taxa única de provisão).
- 4 O sistema gera 3 registros TXT que você precisa publicar no DNS do seu domínio:
| Tipo | Nome (Host) | Valor (template) |
|---|---|---|
| TXT (DKIM) | default._domainkey.meudominio.com.br | v=DKIM1; k=rsa; p=MIGfMA0... |
| TXT (SPF) | meudominio.com.br (apex) | v=spf1 include:_spf.ravinode01.ravimail.com.br ~all |
| TXT (DMARC) | _dmarc.meudominio.com.br | v=DMARC1; p=quarantine; adkim=s; aspf=s |
- 5 No painel do seu DNS (Registro.br, Cloudflare, etc), crie os 3 TXT com os valores exatos que o painel mostra. Propagação: 5min a 48h.
- 6 Volte no painel e clique "Re-verificar". O sistema consulta 3 resolvers públicos em paralelo (8.8.8.8, 1.1.1.1, 9.9.9.9). Quando os 3 registros propagarem, status muda pra verificado.
Bonus: além dos 3 TXT, cada user ganha um subdomínio próprio de tracking (i{userId}.ravimail.com.br) com wildcard DNS+TLS. Isso isola sua reputação dos outros usuários do RaviMail. Configuração automática, zero ação sua.
Seu primeiro envio
3 caminhos. Escolhe o que cabe no seu fluxo. Mesmo token Bearer vale pra todos.
Caminho 1 — API REST
Gere um token em /developers/tokens (sandbox grátis com prefixo rvm_test_*). Mesma rota, escolhe sua linguagem.
curl -X POST https://api.ravimail.com.br/v1/transactional/send \ -H "Authorization: Bearer rvm_test_..." \ -H "Content-Type: application/json" \ -d '{ "to": "joao@empresa.com.br", "from": "noreply@meudominio.com.br", "subject": "Teste", "html": "<p>Olá!</p>" }' # Resposta { "data": { "message_id": "msg_sandbox_xxx", "status": "sent" } }
// PHP 8+ (extensão cURL nativa) $ch = curl_init('https://api.ravimail.com.br/v1/transactional/send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_test_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'to' => 'joao@empresa.com.br', 'from' => 'noreply@meudominio.com.br', 'subject' => 'Teste', 'html' => '<p>Olá!</p>', ]), ]); $data = json_decode(curl_exec($ch), true); // ['data' => ['message_id' => 'msg_sandbox_xxx', 'status' => 'sent']]
// Node 18+ (fetch nativo); ou npm install node-fetch em versões antigas const res = await fetch('https://api.ravimail.com.br/v1/transactional/send', { method: 'POST', headers: { 'Authorization': 'Bearer rvm_test_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ to: 'joao@empresa.com.br', from: 'noreply@meudominio.com.br', subject: 'Teste', html: '<p>Olá!</p>', }), }); const data = await res.json(); // { data: { message_id: 'msg_sandbox_xxx', status: 'sent' } }
# pip install requests import requests res = requests.post( 'https://api.ravimail.com.br/v1/transactional/send', headers={'Authorization': 'Bearer rvm_test_...'}, json={ 'to': 'joao@empresa.com.br', 'from': 'noreply@meudominio.com.br', 'subject': 'Teste', 'html': '<p>Olá!</p>', }, ) data = res.json() # {'data': {'message_id': 'msg_sandbox_xxx', 'status': 'sent'}}
// stdlib: encoding/json + net/http + bytes body, _ := json.Marshal(map[string]string{ "to": "joao@empresa.com.br", "from": "noreply@meudominio.com.br", "subject": "Teste", "html": "<p>Olá!</p>", }) req, _ := http.NewRequest("POST", "https://api.ravimail.com.br/v1/transactional/send", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer rvm_test_...") req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() // {"data":{"message_id":"msg_sandbox_xxx","status":"sent"}}
# stdlib: net/http + json require 'net/http' require 'json' uri = URI('https://api.ravimail.com.br/v1/transactional/send') http = Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = true } req = Net::HTTP::Post.new(uri).tap do |r| r['Authorization'] = 'Bearer rvm_test_...' r['Content-Type'] = 'application/json' r.body = { to: 'joao@empresa.com.br', from: 'noreply@meudominio.com.br', subject: 'Teste', html: '<p>Olá!</p>', }.to_json end data = JSON.parse(http.request(req).body) # {"data" => {"message_id" => "msg_sandbox_xxx", "status" => "sent"}}
Caminho 2 — SMTP Relay
Plugue em Laravel, WordPress, n8n, qualquer cliente SMTP. Mesmo token Bearer como senha SASL PLAIN.
Host: smtp.ravimail.com.br Porta: 587 (STARTTLS obrigatório) Username: api Password: rvm_live_... # token completo no campo senha Auth: PLAIN ou LOGIN
Token precisa do scope smtp:relay. Limite por mensagem: 30 MB. Cobra exatamente igual o envio via API.
Caminho 3 — Painel
Pra disparo de campanha em massa ou envio transacional avulso: Campanhas → Nova Campanha no painel. Selecionar lista, template, agendar ou disparar agora.
Sandbox grátis: tokens com prefixo rvm_test_* não cobram crédito, não enviam de verdade, mas registram tudo. Mailbox Simulator (bounce@simulator.ravimail.com.br) reproduz cenários determinísticos.
SDKs oficiais
Bibliotecas mantidas pela Ravi Systems com cobertura completa dos 155 endpoints da API REST. Idiomáticas em cada linguagem, com tipagem, tratamento de erros granular e verificação HMAC de webhooks builtin. Tudo sob licença MIT.
PHP
PHP 8.1+ · zero deps
composer require
ravisystems/ravimail-php
use Ravimail\Client; $rm = new Client('rvm_live_...'); $rm->transactional->send([ 'to' => 'a@b.com', 'from' => 'c@d.com', 'subject' => 'Hi', 'html' => '<p>Olá</p>', ]);
Node.js
Node 18+ · TypeScript · zero deps
npm install ravimail
import { Client } from 'ravimail'; const rm = new Client('rvm_live_...'); await rm.transactional.send({ to: 'a@b.com', from: 'c@d.com', subject: 'Hi', html: '<p>Olá</p>', });
SDK pra sua linguagem não está aqui ainda? Os 155 endpoints estão documentados em OpenAPI 3.0 em api.ravimail.com.br/v1/openapi.json — você pode gerar client automaticamente em qualquer linguagem com openapi-generator-cli. PRs pra SDKs de outras linguagens são bem-vindos.
Importar contatos por CSV
Formatos aceitos: CSV, TXT, XLSX, XLS. Tamanho máximo: 100 MB por upload.
Passos
- 1 Crie uma lista em Contatos → Listas → Nova Lista (ou use existente).
- 2 Na lista, clique "Importar Contatos". Selecione o arquivo.
- 3 Confirme se a primeira linha é cabeçalho. Mapeie cada coluna:
email(obrigatório),name(opcional),tag(várias colunas viram tags),custom(campos extras com chave própria). - 4 Pipeline automática filtra: emails duplicados no arquivo, contatos já na lista, role-based (
noreply@,info@), emails inválidos, descartáveis (10minutemail, etc) e blocklist global. - 5 Acompanhe o progresso em tempo real (SSE). Ao finalizar, relatório completo mostra quantos importados, rejeitados e o motivo.
Formato CSV esperado
# Exemplo (suporta delimitador `,` ou `;`)
email,nome,tags,telefone
joao@empresa.com.br,João Silva,ativo;premium,11987654321
maria@loja.com.br,Maria Souza,trial,11912345678Via API REST
Max 1000 contatos por request. Dedup automático. Retorna { created, skipped, errors[], total_in }.
curl -X POST https://api.ravimail.com.br/v1/contacts/import \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "list_id": 42, "contacts": [ { "email": "ana@a.com", "name": "Ana", "tags": ["vip"] }, { "email": "bruno@b.com", "fields": {"cidade": "São Paulo"} } ] }'
$ch = curl_init('https://api.ravimail.com.br/v1/contacts/import'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_live_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'list_id' => 42, 'contacts' => [ ['email' => 'ana@a.com', 'name' => 'Ana', 'tags' => ['vip']], ['email' => 'bruno@b.com', 'fields' => ['cidade' => 'São Paulo']], ], ]), ]); $res = json_decode(curl_exec($ch), true);
const res = await fetch('https://api.ravimail.com.br/v1/contacts/import', { method: 'POST', headers: { 'Authorization': 'Bearer rvm_live_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ list_id: 42, contacts: [ { email: 'ana@a.com', name: 'Ana', tags: ['vip'] }, { email: 'bruno@b.com', fields: { cidade: 'São Paulo' } }, ], }), }); const data = await res.json();
import requests res = requests.post( 'https://api.ravimail.com.br/v1/contacts/import', headers={'Authorization': 'Bearer rvm_live_...'}, json={ 'list_id': 42, 'contacts': [ {'email': 'ana@a.com', 'name': 'Ana', 'tags': ['vip']}, {'email': 'bruno@b.com', 'fields': {'cidade': 'São Paulo'}}, ], }, ) data = res.json()
payload := map[string]interface{}{ "list_id": 42, "contacts": []map[string]interface{}{ {"email": "ana@a.com", "name": "Ana", "tags": []string{"vip"}}, {"email": "bruno@b.com", "fields": map[string]string{"cidade": "São Paulo"}}, }, } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.ravimail.com.br/v1/contacts/import", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer rvm_live_...") req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close()
require 'net/http' require 'json' uri = URI('https://api.ravimail.com.br/v1/contacts/import') req = Net::HTTP::Post.new(uri, { 'Authorization' => 'Bearer rvm_live_...', 'Content-Type' => 'application/json', }) req.body = { list_id: 42, contacts: [ { email: 'ana@a.com', name: 'Ana', tags: ['vip'] }, { email: 'bruno@b.com', fields: { cidade: 'São Paulo' } }, ], }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } data = JSON.parse(res.body)
Validação de lista (verificação MX)
Antes de disparar campanha pra uma lista, o sistema exige que ela tenha sido validada nos últimos 30 dias. Lista nova ou stale aparece bloqueada na criação da campanha.
Como rodar
- Na lista, clique "Validar Lista" (botão amarelo quando stale).
- Worker em background consulta DNS MX de cada domínio único da lista (cacheado por 7 dias).
- Marca contatos com MX inexistente como inválido. Polling SSE mostra progresso.
- Ao terminar,
last_validated_atatualiza e a lista volta a ser elegível pra envio.
Revalidação automática: cron diário às 03:15 enfileira jobs pra listas com mais de 25 dias sem validação (margem de 5d antes do hard-block).
Suppression list
Contatos suprimidos não recebem mais email seu, em nenhuma lista. Pré-bloqueio acontece no momento do envio: se está suprimido, retorna erro sem queimar crédito.
Razões automáticas
unsubscribed: contato clicou no link de descadastrobounced: hard bounce permanente (caixa inexistente, domínio inválido)invalid: complaint do destinatário (marcou como spam)
3 operações no mesmo recurso: POST suprimir, GET listar com filtro ?reason=, DELETE reativar (uso cauteloso).
# Suprimir manualmente curl -X POST https://api.ravimail.com.br/v1/suppression \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "ja-saiu@cliente.com", "reason": "unsubscribed" }' # Listar suprimidos por motivo curl -H "Authorization: Bearer rvm_live_..." \ https://api.ravimail.com.br/v1/suppression?reason=bounced # Reativar (cauteloso — só se sabe o que está fazendo) curl -X DELETE -H "Authorization: Bearer rvm_live_..." \ https://api.ravimail.com.br/v1/suppression/ja-saiu@cliente.com
$base = 'https://api.ravimail.com.br/v1/suppression'; $headers = ['Authorization: Bearer rvm_live_...', 'Content-Type: application/json']; // Suprimir $ch = curl_init($base); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode(['email' => 'ja-saiu@cliente.com', 'reason' => 'unsubscribed']), ]); curl_exec($ch); // Listar $list = json_decode(file_get_contents( $base . '?reason=bounced', false, stream_context_create(['http' => ['header' => implode("\r\n", $headers)]]) ), true); // Reativar $ch = curl_init($base . '/ja-saiu@cliente.com'); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, ]); curl_exec($ch);
const base = 'https://api.ravimail.com.br/v1/suppression'; const headers = { 'Authorization': 'Bearer rvm_live_...', 'Content-Type': 'application/json' }; // Suprimir await fetch(base, { method: 'POST', headers, body: JSON.stringify({ email: 'ja-saiu@cliente.com', reason: 'unsubscribed' }), }); // Listar const list = await (await fetch(`${base}?reason=bounced`, { headers })).json(); // Reativar await fetch(`${base}/ja-saiu@cliente.com`, { method: 'DELETE', headers });
import requests base = 'https://api.ravimail.com.br/v1/suppression' headers = {'Authorization': 'Bearer rvm_live_...'} # Suprimir requests.post(base, headers=headers, json={ 'email': 'ja-saiu@cliente.com', 'reason': 'unsubscribed', }) # Listar suppressed = requests.get(base, headers=headers, params={'reason': 'bounced'}).json() # Reativar requests.delete(f'{base}/ja-saiu@cliente.com', headers=headers)
const base = "https://api.ravimail.com.br/v1/suppression" auth := "Bearer rvm_live_..." // Suprimir body, _ := json.Marshal(map[string]string{"email": "ja-saiu@cliente.com", "reason": "unsubscribed"}) req, _ := http.NewRequest("POST", base, bytes.NewReader(body)) req.Header.Set("Authorization", auth) req.Header.Set("Content-Type", "application/json") http.DefaultClient.Do(req) // Listar req, _ = http.NewRequest("GET", base+"?reason=bounced", nil) req.Header.Set("Authorization", auth) res, _ := http.DefaultClient.Do(req) defer res.Body.Close() // Reativar req, _ = http.NewRequest("DELETE", base+"/ja-saiu@cliente.com", nil) req.Header.Set("Authorization", auth) http.DefaultClient.Do(req)
require 'net/http' require 'json' base = 'https://api.ravimail.com.br/v1/suppression' headers = { 'Authorization' => 'Bearer rvm_live_...', 'Content-Type' => 'application/json' } # Suprimir uri = URI(base) req = Net::HTTP::Post.new(uri, headers) req.body = { email: 'ja-saiu@cliente.com', reason: 'unsubscribed' }.to_json Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } # Listar list_uri = URI("#{base}?reason=bounced") list = JSON.parse(Net::HTTP.get(list_uri, headers)) # Reativar del_uri = URI("#{base}/ja-saiu@cliente.com") del_req = Net::HTTP::Delete.new(del_uri, headers) Net::HTTP.start(del_uri.host, del_uri.port, use_ssl: true) { |h| h.request(del_req) }
Criar uma campanha
Resumo dos passos. Pra explicação detalhada com mockups, veja /recursos/campanhas.
- Campanhas → Nova Campanha no painel
- Nome, lista destino (já validada), template (existente ou criar inline)
- Throttle (1-500/min), janela de envio (Send Window), Smart Send (opcional)
- A/B testing (opcional): assunto B + métrica vencedora (open_rate ou click_rate)
- Segmentação por ISP (skipa gmail, microsoft, yahoo, etc se quiser)
- Agendar pra horário futuro ou disparar agora
Smart Send e janela de envio
Send Window (janela de envio) é a faixa horária em que sua campanha pode disparar. Default: Seg-Sex 08:30-18:30 e Sáb 08:30-12:00 (Dom off). Configurável por dia.
Smart Send escolhe a hora ótima de cada contato individualmente: usa o campo contact_scores.best_open_hour (calculado pelo histórico de abertura). Quem nunca abriu cai no Send Window padrão.
Quando usar Smart Send: newsletters, nurturing, campanhas onde o timing afeta abertura. Não usar: promoções com validade em horas (use envio imediato com Send Window curto).
A/B testing
Configure no setup da campanha. Variantes A (controle) e B (teste). Split default 50/50 (configurável 10-90%). Métrica vencedora: open_rate ou click_rate.
Variáveis testáveis: assunto B (obrigatório se ativar A/B) e template B (opcional). Duração do teste: configurável em horas. Quando atinge a duração ou você força via botão, vencedor recebe o resto da base.
Endpoint POST /v1/campaigns/{id}/ab-test pra criar + POST /v1/campaigns/{id}/ab-test/force-winner pra forçar vencedor antes do tempo.
# Configurar A/B curl -X POST https://api.ravimail.com.br/v1/campaigns/142/ab-test \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "variant_b_subject": "Você ganhou: 70% OFF até segunda", "split_percentage": 50, "winner_metric": "click_rate", "duration_hours": 4 }' # Forçar vencedor curl -X POST https://api.ravimail.com.br/v1/campaigns/142/ab-test/force-winner \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "winner": "b" }'
$ch = curl_init('https://api.ravimail.com.br/v1/campaigns/142/ab-test'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_live_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'variant_b_subject' => 'Você ganhou: 70% OFF até segunda', 'split_percentage' => 50, 'winner_metric' => 'click_rate', 'duration_hours' => 4, ]), ]); curl_exec($ch); // Forçar vencedor: mesma estrutura, URL /ab-test/force-winner, body ['winner' => 'b']
const headers = { 'Authorization': 'Bearer rvm_live_...', 'Content-Type': 'application/json', }; // Criar A/B await fetch('https://api.ravimail.com.br/v1/campaigns/142/ab-test', { method: 'POST', headers, body: JSON.stringify({ variant_b_subject: 'Você ganhou: 70% OFF até segunda', split_percentage: 50, winner_metric: 'click_rate', duration_hours: 4, }), }); // Forçar vencedor await fetch('https://api.ravimail.com.br/v1/campaigns/142/ab-test/force-winner', { method: 'POST', headers, body: JSON.stringify({ winner: 'b' }), });
import requests headers = {'Authorization': 'Bearer rvm_live_...'} base = 'https://api.ravimail.com.br/v1/campaigns/142/ab-test' # Criar A/B requests.post(base, headers=headers, json={ 'variant_b_subject': 'Você ganhou: 70% OFF até segunda', 'split_percentage': 50, 'winner_metric': 'click_rate', 'duration_hours': 4, }) # Forçar vencedor requests.post(f'{base}/force-winner', headers=headers, json={'winner': 'b'})
base := "https://api.ravimail.com.br/v1/campaigns/142/ab-test" auth := "Bearer rvm_live_..." // Criar A/B body, _ := json.Marshal(map[string]interface{}{ "variant_b_subject": "Você ganhou: 70% OFF até segunda", "split_percentage": 50, "winner_metric": "click_rate", "duration_hours": 4, }) req, _ := http.NewRequest("POST", base, bytes.NewReader(body)) req.Header.Set("Authorization", auth) req.Header.Set("Content-Type", "application/json") http.DefaultClient.Do(req) // Forçar vencedor fw, _ := json.Marshal(map[string]string{"winner": "b"}) req, _ = http.NewRequest("POST", base+"/force-winner", bytes.NewReader(fw)) req.Header.Set("Authorization", auth) req.Header.Set("Content-Type", "application/json") http.DefaultClient.Do(req)
require 'net/http' require 'json' base = 'https://api.ravimail.com.br/v1/campaigns/142/ab-test' headers = { 'Authorization' => 'Bearer rvm_live_...', 'Content-Type' => 'application/json' } # Criar A/B uri = URI(base) req = Net::HTTP::Post.new(uri, headers) req.body = { variant_b_subject: 'Você ganhou: 70% OFF até segunda', split_percentage: 50, winner_metric: 'click_rate', duration_hours: 4, }.to_json Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } # Forçar vencedor fw_uri = URI("#{base}/force-winner") fw_req = Net::HTTP::Post.new(fw_uri, headers) fw_req.body = { winner: 'b' }.to_json Net::HTTP.start(fw_uri.host, fw_uri.port, use_ssl: true) { |h| h.request(fw_req) }
Sequências automatizadas (drips)
Sequência de N emails com delay configurável entre cada um. 3 tipos de trigger: list.added (contato entrou na lista X), tag.added (recebeu tag Y) ou manual (via API). Detalhes em /recursos/sequencias.
Inscrever contato via API
Idempotente: chamada repetida retorna mesma subscription_id com flag idempotent: true.
curl -X POST https://api.ravimail.com.br/v1/drips/3/subscribe \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "contact_id": 9382 }' # Resposta 1ª chamada { "data": { "id": "sub_8aJ9w2x", "status": "active", "current_step": 0 } } # Mesma chamada de novo { "data": { "id": "sub_8aJ9w2x", "idempotent": true } }
$ch = curl_init('https://api.ravimail.com.br/v1/drips/3/subscribe'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_live_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['contact_id' => 9382]), ]); $sub = json_decode(curl_exec($ch), true)['data']; // $sub['id'] = 'sub_8aJ9w2x'; idempotent na 2ª chamada
const res = await fetch('https://api.ravimail.com.br/v1/drips/3/subscribe', { method: 'POST', headers: { 'Authorization': 'Bearer rvm_live_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ contact_id: 9382 }), }); const { data: sub } = await res.json(); // sub.id = 'sub_8aJ9w2x'; sub.idempotent = true na 2ª chamada
import requests res = requests.post( 'https://api.ravimail.com.br/v1/drips/3/subscribe', headers={'Authorization': 'Bearer rvm_live_...'}, json={'contact_id': 9382}, ) sub = res.json()['data'] # sub['id'] == 'sub_8aJ9w2x'; sub['idempotent'] na 2ª chamada
body, _ := json.Marshal(map[string]int{"contact_id": 9382}) req, _ := http.NewRequest("POST", "https://api.ravimail.com.br/v1/drips/3/subscribe", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer rvm_live_...") req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() // Decode response.Body para pegar data.id e data.idempotent
require 'net/http' require 'json' uri = URI('https://api.ravimail.com.br/v1/drips/3/subscribe') req = Net::HTTP::Post.new(uri, { 'Authorization' => 'Bearer rvm_live_...', 'Content-Type' => 'application/json', }) req.body = { contact_id: 9382 }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } sub = JSON.parse(res.body)['data'] # sub['id'] == 'sub_8aJ9w2x'; sub['idempotent'] na 2ª chamada
Worker cron de 1 minuto avança subscriptions. Delay calculado desde a inscrição, não desde o step anterior. Cap: sem limite formal (UNIQUE em drip_id + step_number garante ordem).
Webhooks HMAC
Receba eventos em tempo real no seu sistema. Eventos disponíveis: transactional.sent, delivered, opened, clicked, bounced, complained, unsubscribed.
Cadastrar endpoint
A resposta inclui o signing_secret apenas uma vez — guarda em local seguro, não aparece de novo.
curl -X POST https://api.ravimail.com.br/v1/webhooks/endpoints \ -H "Authorization: Bearer rvm_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.io/webhooks/ravimail", "events": ["transactional.delivered", "transactional.bounced"], "description": "Webhook principal de produção" }' # Resposta { "data": { "id": "wh_xyz", "signing_secret": "whsec_..." } }
$ch = curl_init('https://api.ravimail.com.br/v1/webhooks/endpoints'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_live_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'url' => 'https://app.io/webhooks/ravimail', 'events' => ['transactional.delivered', 'transactional.bounced'], 'description' => 'Webhook principal de produção', ]), ]); $wh = json_decode(curl_exec($ch), true)['data']; // $wh['signing_secret'] — salva AGORA, não aparece de novo
const res = await fetch('https://api.ravimail.com.br/v1/webhooks/endpoints', { method: 'POST', headers: { 'Authorization': 'Bearer rvm_live_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ url: 'https://app.io/webhooks/ravimail', events: ['transactional.delivered', 'transactional.bounced'], description: 'Webhook principal de produção', }), }); const { data: wh } = await res.json(); // wh.signing_secret — salva AGORA, não aparece de novo
import requests res = requests.post( 'https://api.ravimail.com.br/v1/webhooks/endpoints', headers={'Authorization': 'Bearer rvm_live_...'}, json={ 'url': 'https://app.io/webhooks/ravimail', 'events': ['transactional.delivered', 'transactional.bounced'], 'description': 'Webhook principal de produção', }, ) wh = res.json()['data'] # wh['signing_secret'] — salva AGORA, não aparece de novo
payload, _ := json.Marshal(map[string]interface{}{ "url": "https://app.io/webhooks/ravimail", "events": []string{"transactional.delivered", "transactional.bounced"}, "description": "Webhook principal de produção", }) req, _ := http.NewRequest("POST", "https://api.ravimail.com.br/v1/webhooks/endpoints", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer rvm_live_...") req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() // Salva data.signing_secret — não aparece de novo
require 'net/http' require 'json' uri = URI('https://api.ravimail.com.br/v1/webhooks/endpoints') req = Net::HTTP::Post.new(uri, { 'Authorization' => 'Bearer rvm_live_...', 'Content-Type' => 'application/json', }) req.body = { url: 'https://app.io/webhooks/ravimail', events: ['transactional.delivered', 'transactional.bounced'], description: 'Webhook principal de produção', }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } wh = JSON.parse(res.body)['data'] # wh['signing_secret'] — salva AGORA, não aparece de novo
Verificar assinatura HMAC
Header X-Ravimail-Signature tem formato sha256=<hex>. Use comparação constant-time (não ==) pra evitar timing attack.
$sig = $_SERVER['HTTP_X_RAVIMAIL_SIGNATURE'] ?? ''; $body = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $body, $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit; } // payload válido — processa $body
import { createHmac, timingSafeEqual } from 'crypto'; function verify(rawBody, signature, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature || ''); return a.length === b.length && timingSafeEqual(a, b); } // Express: precisa de bodyParser.raw() pra ter req.rawBody
import hmac, hashlib def verify(raw_body: bytes, signature: str, secret: str) -> bool: expected = 'sha256=' + hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) # Flask: use request.get_data(cache=False) pra raw bytes
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" ) func Verify(rawBody []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) }
require 'openssl' require 'rack/utils' def verify(raw_body, signature, secret) expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body) Rack::Utils.secure_compare(expected, signature.to_s) end # Rails: use request.raw_post pra raw body
# Pra TESTAR seu endpoint manualmente: BODY='{"event":"transactional.delivered","message_id":"msg_x"}' SECRET='whsec_...' SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')" curl -X POST https://seu-app.io/webhooks/ravimail \ -H "X-Ravimail-Signature: $SIG" \ -H "Content-Type: application/json" \ -d "$BODY"
Cabeçalho X-Ravimail-Signature-V2 contém variante SHA-512 (recomendado em casos de alta sensibilidade — troque sha256 por sha512 nos exemplos).
Retry e replay manual
Cada evento tenta entrega até 12 vezes com backoff exponencial: 1s · 5s · 30s · 2min · 10min · 30min · 1h · 2h · 4h · 8h · 16h · 24h. Total: até 60 horas de retry antes de marcar como exhausted.
Após 10 falhas consecutivas, o endpoint vira marked_failing_at e fica destacado no dashboard. Investigue em /developers/webhooks/{id}: lista 12 tentativas com response_status, body, timing.
Replay manual: botão "Retransmitir" em qualquer delivery falhada cria nova tentativa #1 (zera o retry chain).
Event stream (SSE)
Alternativa pra webhooks: stream Server-Sent Events com eventos em tempo real. Conexão persistente, sem polling. Útil pra dashboards reativos.
Filtros opcionais via query string: ?event=transactional.bounced&domain=meu.com.br&campaign=142. Conexão persistente — implemente backoff se cair.
# --no-buffer mantém o stream aberto e imprime evento a evento curl --no-buffer \ -H "Authorization: Bearer rvm_live_..." \ -H "Accept: text/event-stream" \ https://api.ravimail.com.br/v1/events/stream # Cada evento vem como: # event: transactional.delivered # data: {"message_id":"msg_xyz",...}
// Browser tem EventSource nativo; em Node use 'eventsource' (npm install eventsource) import EventSource from 'eventsource'; const es = new EventSource('https://api.ravimail.com.br/v1/events/stream', { headers: { 'Authorization': 'Bearer rvm_live_...' }, }); es.onmessage = (e) => console.log(JSON.parse(e.data)); es.onerror = (err) => console.error('reconectando...', err);
// Stream com cURL + WRITEFUNCTION (callback por chunk) $ch = curl_init('https://api.ravimail.com.br/v1/events/stream'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_live_...', 'Accept: text/event-stream', ], CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $line) { if (str_starts_with($line, 'data: ')) { $event = json_decode(substr($line, 6), true); var_dump($event); } } return strlen($chunk); }, CURLOPT_TIMEOUT => 0, ]); curl_exec($ch);
# pip install sseclient-py requests import requests from sseclient import SSEClient res = requests.get( 'https://api.ravimail.com.br/v1/events/stream', headers={'Authorization': 'Bearer rvm_live_...', 'Accept': 'text/event-stream'}, stream=True, ) import json for evt in SSEClient(res).events(): print(json.loads(evt.data))
// bufio.Scanner lê linha a linha; bibliotecas dedicadas: r3labs/sse req, _ := http.NewRequest("GET", "https://api.ravimail.com.br/v1/events/stream", nil) req.Header.Set("Authorization", "Bearer rvm_live_...") req.Header.Set("Accept", "text/event-stream") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() sc := bufio.NewScanner(res.Body) for sc.Scan() { line := sc.Text() if strings.HasPrefix(line, "data: ") { fmt.Println(line[6:]) } }
# gem install ld-eventsource require 'ld-eventsource' uri = URI('https://api.ravimail.com.br/v1/events/stream') sse = SSE::Client.new(uri, headers: { 'Authorization' => 'Bearer rvm_live_...' }) do |client| client.on_event do |event| puts JSON.parse(event.data) end end
Autenticação da API
Header obrigatório em todos os endpoints /v1/*:
Authorization: Bearer rvm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Tipos de token
rvm_live_*— produção. Cobra crédito, envia de verdade.rvm_test_*— sandbox. Sem cobrança, sem envio real, mas registra tudo e dispara webhooks reais.
40+ scopes granulares
Cada token tem só os scopes que você der. Exemplos: transactional:send, contacts:read, campaigns:write, webhooks:admin, suppression:write, smtp:relay. Veja a lista completa em /developers/tokens.
IP whitelist: cada token aceita lista CIDR (192.168.0.0/24) ou IPs individuais. Bloqueia chamadas fora da allowlist mesmo com token válido. Camada extra de segurança em produção.
Sandbox e Mailbox Simulator
Token com prefixo rvm_test_* ativa modo sandbox automaticamente: zero crédito, zero envio real, mas com tudo o resto funcionando (webhook, tracking, supression).
Endereços determinísticos
success@simulator.ravimail.com.br # delivered bounce@simulator.ravimail.com.br # hard bounce soft-bounce@simulator.ravimail.com.br # soft bounce + retry complaint@simulator.ravimail.com.br # marca como spam suppress@simulator.ravimail.com.br # vai pra suppression opened@simulator.ravimail.com.br # delivered + open clicked@simulator.ravimail.com.br # delivered + open + click
Trigger manual de evento
Pra testar handler de webhook sem esperar evento real, dispara manualmente em qualquer message_id de sandbox.
curl -X POST https://api.ravimail.com.br/v1/sandbox/trigger-event \ -H "Authorization: Bearer rvm_test_..." \ -H "Content-Type: application/json" \ -d '{ "message_id": "msg_sb_xy", "event": "bounced" }'
$ch = curl_init('https://api.ravimail.com.br/v1/sandbox/trigger-event'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer rvm_test_...', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['message_id' => 'msg_sb_xy', 'event' => 'bounced']), ]); curl_exec($ch);
await fetch('https://api.ravimail.com.br/v1/sandbox/trigger-event', { method: 'POST', headers: { 'Authorization': 'Bearer rvm_test_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ message_id: 'msg_sb_xy', event: 'bounced' }), });
import requests requests.post( 'https://api.ravimail.com.br/v1/sandbox/trigger-event', headers={'Authorization': 'Bearer rvm_test_...'}, json={'message_id': 'msg_sb_xy', 'event': 'bounced'}, )
body, _ := json.Marshal(map[string]string{ "message_id": "msg_sb_xy", "event": "bounced", }) req, _ := http.NewRequest("POST", "https://api.ravimail.com.br/v1/sandbox/trigger-event", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer rvm_test_...") req.Header.Set("Content-Type", "application/json") http.DefaultClient.Do(req)
require 'net/http' require 'json' uri = URI('https://api.ravimail.com.br/v1/sandbox/trigger-event') req = Net::HTTP::Post.new(uri, { 'Authorization' => 'Bearer rvm_test_...', 'Content-Type' => 'application/json', }) req.body = { message_id: 'msg_sb_xy', event: 'bounced' }.to_json Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
Vincular conta CNPJ Base
CNPJ Base é uma base de 27M+ empresas brasileiras pesquisáveis por UF, CNAE, porte, etc. Integração nativa pra importar leads B2B prontos pra prospectar.
- No painel, Contatos → CNPJ Base → Vincular conta
- Opções: criar conta nova (auto-cadastro), logar com conta existente, ou colar token manual
- Após vincular, o painel mostra busca integrada (filtros UF, município, CNAE, porte, faixa de funcionários, contatos com email/site/telefone)
Buscar e importar empresas
Após vincular, busca via Contatos → CNPJ Base → Buscar. Define filtros (ex: "Comércio varejista de roupas em São Paulo, com email"), preview dos resultados, seleciona quais quer importar e a lista destino. Importação cria contatos normais (vão pra suppression, tags, drips, campanhas).
Filtros suportados: UF, município, CNAE primário/secundário, porte (ME, EPP, etc), natureza jurídica, presença de email, telefone, site, range de CEP, sócios.
Bounce alto ou IP pausado
Cada IP novo passa por warmup de 7 dias com volume crescente por provedor. Se a taxa de bounce de um bucket (gmail, microsoft, yahoo, icloud, other) ultrapassa o threshold, o sistema pausa esse bucket automaticamente e retoma no vira-dia (00:30 UTC).
Como você vê
- Campanha mostra status "Aguardando dia seguinte" (amber) quando 100% dos pendentes estão em buckets pausados
- Notificação WhatsApp se você tiver configurado o alert
warmup_paused - Dashboard de reputação em
/admin/nodes/{id}mostra score por IP/bucket
Como resolver
- Vira-dia retoma automaticamente (não precisa fazer nada se foi pause de threshold diário)
- Auditar lista: lista com muitos endereços inválidos vai degradar reputação. Use validação MX + suppression antes de disparar grande
- Auditar conteúdo via spam score
Webhook não chega no seu sistema
Abra /developers/webhooks/{id}. Tab "Entregas recentes" lista as últimas tentativas com response_status HTTP, response_time_ms, body retornado pelo seu endpoint.
Causas comuns
- Timeout 15s: seu endpoint demora demais. Responda 200 OK rápido e processe assíncrono.
- HTTP 5xx: erro no seu app. O sistema retenta automaticamente (12 vezes, backoff exponencial).
- HTTP 401/403: você está bloqueando algo (autenticação errada no middleware).
- DNS resolve falhou: domínio do seu webhook está fora ou inválido.
- Status
exhausted: esgotou os 12 retries. Botão "Retransmitir" cria nova tentativa #1.
Spam score alto / mail-tester baixo
Antes de disparar, o sistema avalia o template com heurística própria (0-100). Score ≤ 20 = baixo risco; 21-50 = moderado; 51-75 = alto; 76+ = crítico.
Causas detectadas automaticamente
- Assunto vazio (+25)
- Mais de 50% em CAPS no assunto (+15)
- Sem link de unsubscribe (+5)
- Body só com imagem, sem texto (+20)
- Links HTTP sem TLS (+15)
- Palavras-gatilho (PT-BR e EN): "ganhe dinheiro", "clique aqui", "oferta imperdível", etc. (+30)
- Excesso de formatação (negrito/cores) (+8)
Como melhorar
- Assunto descritivo, < 50% CAPS, máximo 1-2 exclamações
- Ratio texto/HTML > 0.6 (mais texto que código)
- Sempre
{{unsubscribe_url}}no template (obrigatório por LGPD) - Use HTTPS em todos os links
- Evite palavras-gatilho. Use linguagem direta
Falso positivo conhecido: HTML_IMAGE_ONLY_08 no mail-tester aparece quando você usa o pixel de tracking (1x1 PNG inevitável). Score 8-8.5 com esse único flag = OK, sem ação necessária.
Domínio em blacklist (RBL)
O sistema consulta diariamente 4 RBLs principais: Spamhaus DBL (decisivo sozinho), SURBL, URIBL, RBL.JP (precisam 2 pra dar match). Se seu domínio listou, o envio é auto-pausado em todos os nós.
Como você vê
- Alert vermelho no painel: "Domínio em RBL externo"
- Notificação WhatsApp se você configurou alert
domain_blocked - Campo
domain_pause_eventsno histórico do domínio mostra qual RBL listou e quando
Como resolver
- Investigue causa raiz: conteúdo, alguma campanha gerou complaints, DKIM/SPF inválido
- Solicite delisting manual no site da RBL (link específico por blacklist)
- Após confirmar que saiu da RBL, peça resume manual ao suporte (ação manual com checkbox de ACK)
Resume não é automático. Isso é proposital: queremos que você tenha agido sobre a causa raiz antes de voltar a enviar.
Email "entregue" mas não aparece na inbox
Delivered no sistema significa que o provedor (Gmail, Outlook) aceitou a mensagem com 250 OK. Não garante que foi pra inbox primária — pode ter ido pra promoções, spam, ou filtro corporativo.
Ferramentas pra investigar
- Google Postmaster Tools: cadastro manual em postmaster.google.com. Mostra spam rate, autenticação DKIM/SPF, delivery stats por domínio.
- Microsoft SNDS: Junk Mail Report Program pra IPs (não pra domínio). Útil pra disparos pra Hotmail/Outlook/Live.
- Tracking de open: se o pixel não dispara, provavelmente foi pra spam. Veja open rate por provedor no relatório da campanha.
Causas comuns: DKIM/SPF inválido (verifique no painel), conteúdo com spam score alto, IP novo sem reputação (warmup ainda rampando), domínio em RBL.
Falha de pagamento
Pagamentos via Mercado Pago (PIX e cartão de crédito). PIX expira em 30 minutos: se passou, gere um novo. Cartão recusado: o painel mostra motivo retornado pelo emissor.
O que fazer
- Ver histórico em /billing/history com status de cada transação
- PIX expirado: clica "Gerar novo PIX" — código novo válido por mais 30 min
- Cartão recusado: tente outro cartão (ou use PIX)
- Notificação WhatsApp se você configurou alert
payment_failed
Assinatura recorrente: em caso de falha do cartão, o Mercado Pago tenta automaticamente até 3 vezes em dias diferentes. Se todas falharem, o serviço entra em modo limitado (sem novos disparos, mas dados preservados).
Migrar do Mailchimp
Veja comparativo completo em /vs/mailchimp. Resumo do fluxo de migração:
- Exporte Audience do Mailchimp: Audience → All contacts → Export Audience (CSV). Aguarde email com link.
- Importe no RaviMail: crie lista nova, use o CSV exportado (veja Importar CSV).
- Refaça templates: sintaxe de merge tag diferente. Mailchimp usa
*|FNAME|*, RaviMail usa{{nome}}. Variáveis principais:{{email}},{{name}},{{unsubscribe_url}}. - Migre suppression: exporte Unsubscribed Contacts do Mailchimp, importe via
POST /v1/suppressionem batches. - Aponte DNS do domínio remetente: veja Criar conta e verificar domínio.
Migrar do SendGrid
Veja comparativo em /vs/sendgrid. Migração transacional via API é a mais direta:
- Payload é similar: SendGrid usa
personalizations[], RaviMail usatodireto. Renomeiefrom,subject,content[]→html. - Templates dinâmicos: SendGrid
{{handlebars}}= RaviMail{{mustache}}. Compatível na maioria dos casos. - Webhooks: formato JSON diferente. Reescreva o handler conforme nossa documentação em Webhooks HMAC.
- API Keys: não migrável diretamente, gere novos tokens em
/developers/tokenscom os scopes necessários. - Suppression: exporte Bounces + Blocks + Spam Reports + Unsubscribes do SendGrid, importe via
POST /v1/suppression.
Migrar do RD Station
Veja comparativo em /vs/rdstation. RD Station tem alguns ângulos específicos:
- Exportar contatos: Configurações → Exportar Base. Inclui Lead Score (vamos chamar de score). Tags viram tags.
- Importar no RaviMail: CSV padrão. Score do RD pode entrar como custom field
score_anterior. - Funil: RD usa "Etapas". Recrie no nosso funil de vendas em
/funnel/stages. - Automações: RD usa "Fluxos". Recrie em /drips com mesma sequência de emails + delays.
- API: a do RD Light não existe. Se você tinha integração com Pro/Basic, código quase totalmente novo, mas com 155 endpoints documentados em /api.
Não encontrou o que procurava?
Use a referência completa da API com os 155 endpoints, veja o histórico de mudanças no changelog ou fale direto com o suporte.