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/v1 com 155 endpoints documentados
  • SMTP Relay em smtp.ravimail.com.br:587 com 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. 1 No painel, Domínios → Adicionar domínio. Digite só o domínio (ex: meudominio.com.br, sem https://).
  2. 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. 3 Pague R$ 97 via PIX (taxa única de provisão).
  4. 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.brv=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.brv=DMARC1; p=quarantine; adkim=s; aspf=s
  1. 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.
  2. 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

Packagist v1.0.0 license MIT
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 v1.0.0 license MIT
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>',
});

Python

Python 3.9+ · type hints

PyPI v1.0.0 license MIT
pip install ravimail
from ravimail import Client

rm = Client('rvm_live_...')
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. 1 Crie uma lista em Contatos → Listas → Nova Lista (ou use existente).
  2. 2 Na lista, clique "Importar Contatos". Selecione o arquivo.
  3. 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. 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. 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,11912345678

Via 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

  1. Na lista, clique "Validar Lista" (botão amarelo quando stale).
  2. Worker em background consulta DNS MX de cada domínio único da lista (cacheado por 7 dias).
  3. Marca contatos com MX inexistente como inválido. Polling SSE mostra progresso.
  4. Ao terminar, last_validated_at atualiza 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 descadastro
  • bounced: 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.

  1. Campanhas → Nova Campanha no painel
  2. Nome, lista destino (já validada), template (existente ou criar inline)
  3. Throttle (1-500/min), janela de envio (Send Window), Smart Send (opcional)
  4. A/B testing (opcional): assunto B + métrica vencedora (open_rate ou click_rate)
  5. Segmentação por ISP (skipa gmail, microsoft, yahoo, etc se quiser)
  6. 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.

  1. No painel, Contatos → CNPJ Base → Vincular conta
  2. Opções: criar conta nova (auto-cadastro), logar com conta existente, ou colar token manual
  3. 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_events no histórico do domínio mostra qual RBL listou e quando

Como resolver

  1. Investigue causa raiz: conteúdo, alguma campanha gerou complaints, DKIM/SPF inválido
  2. Solicite delisting manual no site da RBL (link específico por blacklist)
  3. 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

  1. Ver histórico em /billing/history com status de cada transação
  2. PIX expirado: clica "Gerar novo PIX" — código novo válido por mais 30 min
  3. Cartão recusado: tente outro cartão (ou use PIX)
  4. 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:

  1. Exporte Audience do Mailchimp: Audience → All contacts → Export Audience (CSV). Aguarde email com link.
  2. Importe no RaviMail: crie lista nova, use o CSV exportado (veja Importar CSV).
  3. Refaça templates: sintaxe de merge tag diferente. Mailchimp usa *|FNAME|*, RaviMail usa {{nome}}. Variáveis principais: {{email}}, {{name}}, {{unsubscribe_url}}.
  4. Migre suppression: exporte Unsubscribed Contacts do Mailchimp, importe via POST /v1/suppression em batches.
  5. 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:

  1. Payload é similar: SendGrid usa personalizations[], RaviMail usa to direto. Renomeie from, subject, content[]html.
  2. Templates dinâmicos: SendGrid {{handlebars}} = RaviMail {{mustache}}. Compatível na maioria dos casos.
  3. Webhooks: formato JSON diferente. Reescreva o handler conforme nossa documentação em Webhooks HMAC.
  4. API Keys: não migrável diretamente, gere novos tokens em /developers/tokens com os scopes necessários.
  5. 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:

  1. Exportar contatos: Configurações → Exportar Base. Inclui Lead Score (vamos chamar de score). Tags viram tags.
  2. Importar no RaviMail: CSV padrão. Score do RD pode entrar como custom field score_anterior.
  3. Funil: RD usa "Etapas". Recrie no nosso funil de vendas em /funnel/stages.
  4. Automações: RD usa "Fluxos". Recrie em /drips com mesma sequência de emails + delays.
  5. 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.