Cobrança Automatizada — Documentação Completa
Documentação do workflow de cobrança mensal automatizada integrando Google Sheets, Asaas (boleto + PIX) e Gmail, orquestrado pelo n8n hospedado em VPS própria.
Visão Geral
O sistema lê uma planilha do Google Sheets com os dados dos clientes, gera cobranças individuais no Asaas (boleto e PIX) e envia um e-mail personalizado para cada cliente com os links de pagamento. O processo roda automaticamente todo dia 10 de cada mês às 8h, com intervalo de 1 minuto entre cada envio.
Fluxo Resumido
Schedule (dia 10, 8h)
└─ Google Sheets (lê clientes)
└─ Loop (1 por vez)
└─ Nome vazio? → [encerra]
└─ Preparar Dados
└─ E-mail vazio? → [pula, aguarda 1min]
└─ Tem Customer ID?
├─ SIM → usa ID existente
└─ NÃO → Criar Cliente Asaas
└─ Salvar ID na planilha
└─ Criar Boleto + Criar PIX (paralelo)
└─ Montar Links Pagamento
└─ Montar HTML E-mail
└─ Enviar E-mail (Gmail)
└─ Aguardar 1 minuto
└─ [volta ao Loop]
Pré-requisitos
- n8n versão 2.9+ (self-hosted)
- Conta Google com acesso à planilha e Gmail
- Conta no Asaas Sandbox para testes
- Credenciais configuradas no n8n:
- Google Sheets OAuth2
- Gmail OAuth2
- Asaas API Key (HTTP Header Auth)
Estrutura da Planilha
A planilha deve estar no Google Sheets com a aba chamada Clientes e as seguintes colunas obrigatórias:
| Coluna | Obrigatório | Descrição |
|---|---|---|
Nome |
✅ | Nome completo do cliente |
e-mail |
✅ | E-mail para envio da cobrança |
Plano |
✅ | Nome do plano contratado |
Valor |
✅ | Valor da assinatura (ex: R$ 29,90) |
Data de vencimento |
✅ | Data no formato DD/MM/YYYY |
CPF/CNPJ |
✅ | Documento do cliente (somente números) |
Asaas Customer ID |
✅ | Deixar vazio — preenchido automaticamente na primeira execução |
!!! warning "Importante"
A coluna Asaas Customer ID deve existir na planilha mesmo que vazia. O workflow a preenche automaticamente na primeira vez que processa cada cliente e reutiliza nas execuções seguintes, evitando duplicatas no Asaas.
!!! tip "Encerrando o loop"
O loop encerra quando encontra uma linha com Nome vazio. Mantenha as linhas de clientes contíguas sem linhas em branco no meio.
Credenciais no n8n
Google Sheets e Gmail (OAuth2)
- No n8n, vá em Settings → Credentials → Add Credential
- Selecione Google Sheets OAuth2 API
- Autentique com sua conta Google
- Repita o processo para Gmail OAuth2
Asaas API Key (HTTP Header Auth)
- No Asaas Sandbox: Minha Conta → Integrações → API Key
- Copie a chave
- No n8n, crie uma credencial do tipo HTTP Header Auth:
- Name:
Asaas API Key - Header Name:
access_token - Header Value:
sua_api_key_aqui
- Name:
Nós do Workflow
1. Schedule — Dia 10 às 8h
Tipo: Schedule Trigger
Dispara automaticamente todo dia 10 do mês às 8h via expressão cron.
0 8 10 * *
2. Google Sheets — Clientes
Tipo: Google Sheets
Operação: Read All Rows
Lê todas as linhas da aba Clientes. Retorna um item por linha, incluindo o campo row_number que é usado posteriormente para atualizar a linha correta com o Asaas Customer ID.
Configuração:
- Document ID: ID da sua planilha (extraído da URL do Google Sheets)
- Sheet Name:
Clientes
3. Loop — Um por vez
Tipo: Split In Batches
Batch Size: 1
Processa um cliente por vez. Possui duas saídas:
- Saída 0 (done): acionada quando todos os itens foram processados — não conectada a nada, encerra o fluxo
- Saída 1 (loop): acionada a cada iteração — conectada ao próximo nó
!!! warning "Atenção nas conexões"
No n8n, a saída loop é o índice 1 e a saída done é o índice 0. Verifique no canvas que o fio sai da porta loop para o nó seguinte.
4. Nome está vazio?
Tipo: IF
Verifica se o campo Nome da linha atual está preenchido.
- Condição:
{{ ($json['Nome'] ?? '').toString().trim() }}→is not empty - Saída true (tem nome): segue o fluxo
- Saída false (nome vazio): não conectada — encerra o loop
5. Preparar Dados
Tipo: Code (JavaScript)
Normaliza e formata todos os campos necessários para os próximos nós. Realiza a conversão da data de DD/MM/YYYY para YYYY-MM-DD (formato exigido pelo Asaas) e remove caracteres não numéricos do CPF/CNPJ e do valor.
Campos gerados:
| Campo | Descrição |
|---|---|
nome |
Nome normalizado |
email |
E-mail normalizado |
plano |
Nome do plano |
cpfCnpj |
Somente números |
valorNumerico |
Float limpo (ex: 29.9) |
valorFormatado |
String BRL (ex: R$ 29,90) |
dataVencimento |
Data original (DD/MM/YYYY) |
dueDateISO |
Data convertida (YYYY-MM-DD) |
subject |
Assunto do e-mail |
row_number |
Número da linha na planilha |
Código:
const item = $input.first().json;
const valorRaw = String(item['Valor'] || '0').replace(/[^0-9,.]/g, '').replace(',', '.');
const valorNumerico = parseFloat(valorRaw) || 0;
const valorFormatado = valorNumerico.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
const nome = (item['Nome'] || '').toString().trim();
const email = (item['e-mail'] || item['Email'] || '').toString().trim();
const plano = (item['Plano'] || '').toString().trim();
const cpfCnpj = (item['CPF/CNPJ'] || '').toString().replace(/[^0-9]/g, '').trim();
const rowNumber = item['row_number'] || '';
const dataRaw = (item['Data de vencimento'] || '').toString().trim();
let dueDateISO = dataRaw;
if (dataRaw.includes('/')) {
const [d, m, y] = dataRaw.split('/');
dueDateISO = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}`;
}
const subject = `Lembrete de Vencimento - ${plano} | Vence em ${dataRaw}`;
return [{ json: {
...item,
nome, email, plano, cpfCnpj, rowNumber,
valorNumerico, valorFormatado,
dataVencimento: dataRaw, dueDateISO,
subject
}}];
6. E-mail está vazio?
Tipo: IF
Verifica se o cliente tem e-mail cadastrado.
- Condição:
={{ ($json.email ?? '').toString().trim() }}→is not empty - Saída true (tem e-mail): segue para a criação do Customer ID
- Saída false (sem e-mail): vai direto para
Aguardar 1 minutoe volta ao loop sem enviar
7. Tem Customer ID?
Tipo: IF
Verifica se o cliente já foi cadastrado no Asaas em uma execução anterior.
- Condição:
={{ ($json['Asaas Customer ID'] ?? '').toString().trim() }}→is not empty - Saída true: usa o ID existente, vai direto para criar as cobranças
- Saída false: cria o cliente no Asaas primeiro
8. Criar Cliente Asaas
Tipo: HTTP Request
Método: POST
URL: https://sandbox.asaas.com/api/v3/customers
Cria o cliente no Asaas. Retorna o objeto com o campo id (cus_xxx) que será salvo na planilha.
Body (JSON):
{
"name": "={{ $json.nome }}",
"cpfCnpj": "={{ $json.cpfCnpj }}",
"email": "={{ $json.email }}"
}
Credencial: HTTP Header Auth com access_token
!!! info "Ambiente de produção"
Para produção, troque a URL base para https://api.asaas.com/api/v3/customers e use a API key de produção.
9. Salvar Customer ID
Tipo: Google Sheets
Operação: Update Row
Salva o id retornado pelo Asaas na coluna Asaas Customer ID da linha correspondente.
Configuração:
- Sheet Name:
Clientes - Column to Match On:
row_number - Fields to Update:
Asaas Customer ID→={{ $('Criar Cliente Asaas').item.json.id }}
!!! warning "Erro comum"
Se aparecer Sheet with ID Clientes not found, verifique se o nome da aba na planilha é exatamente Clientes (com C maiúsculo).
10. Criar Boleto / Criar PIX
Tipo: HTTP Request (dois nós paralelos)
Método: POST
URL: https://sandbox.asaas.com/api/v3/payments
Os dois nós são idênticos, diferindo apenas no campo billingType.
Body (JSON):
{
"customer": "={{ $json['Asaas Customer ID'] !== '' ? $json['Asaas Customer ID'] : $json.id }}",
"billingType": "BOLETO",
"value": "={{ $('Preparar Dados').item.json.valorNumerico }}",
"dueDate": "={{ $('Preparar Dados').item.json.dueDateISO }}",
"description": "={{ 'Assinatura ' + $('Preparar Dados').item.json.plano }}"
}
Para o PIX, troque "billingType": "BOLETO" por "billingType": "PIX".
!!! tip "Por que referenciar Preparar Dados diretamente?"
Após o nó Merge, o $json passa a conter os dados da API do Asaas. Para acessar os dados do cliente (valor, data, plano), é necessário referenciar o nó Preparar Dados explicitamente com $('Preparar Dados').item.json.
!!! info "bankSlipUrl no sandbox"
No ambiente sandbox do Asaas, o campo bankSlipUrl sempre retorna null. Use o invoiceUrl para ambos os botões de pagamento. Em produção, o bankSlipUrl é preenchido normalmente.
11. Unificar Boleto+PIX
Tipo: Merge
Mode: Combine
Combination Mode: Merge By Position
Une os resultados dos dois nós de pagamento (Boleto e PIX) em um único item para o próximo nó.
- Input 0: resultado do Criar Boleto
- Input 1: resultado do Criar PIX
!!! warning "Configuração crítica"
Use Merge By Position, não Combine By Fields. O modo Combine By Fields exige campos de correspondência que não fazem sentido aqui.
12. Montar Links Pagamento
Tipo: Code (JavaScript)
Extrai os links de pagamento gerados pelo Asaas e resgata os dados do cliente do nó Preparar Dados para montar o payload completo do e-mail.
const boleto = $input.first().json;
const pix = $input.last().json;
const dados = $('Preparar Dados').item.json;
return [{
json: {
email: dados.email,
nome: dados.nome,
plano: dados.plano,
valorFormatado: dados.valorFormatado,
dataVencimento: dados.dataVencimento,
subject: dados.subject,
linkBoleto: boleto.invoiceUrl || '',
linkPix: pix.invoiceUrl || ''
}
}];
!!! warning "Nó Set vs Code"
O nó Set pode tratar expressões como texto literal se o toggle = não estiver ativado em cada campo individualmente. Prefira o nó Code para evitar esse problema.
13. Montar HTML E-mail
Tipo: Code (JavaScript)
Monta o HTML completo do e-mail com os dados do cliente já interpolados via template literal do JavaScript. Isso garante que os valores são resolvidos no código, sem depender das expressões {{ }} do n8n dentro de strings HTML.
const item = $input.first().json;
const htmlBody = `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; }
.container { max-width: 600px; margin: 30px auto; background: #fff; border-radius: 8px; overflow: hidden; }
.header { background: #1a73e8; padding: 30px; text-align: center; }
.header h1 { color: #fff; margin: 0; font-size: 22px; }
.body { padding: 30px; }
.body p { color: #333; font-size: 15px; line-height: 1.6; }
.info-box { background: #f0f7ff; border-left: 4px solid #1a73e8; border-radius: 4px; padding: 16px 20px; margin: 20px 0; }
.info-box p { margin: 6px 0; color: #222; font-size: 15px; }
.info-box strong { color: #1a73e8; }
.btn { display: inline-block; margin: 8px 4px; background: #1a73e8; color: #fff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-size: 15px; font-weight: bold; }
.btn-pix { background: #32a852; }
.btn-wrap { text-align: center; margin: 24px 0; }
.footer { background: #f4f4f4; text-align: center; padding: 18px; font-size: 12px; color: #888; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>Lembrete de Vencimento</h1></div>
<div class="body">
<p>Olá, <strong>${item.nome}</strong>!</p>
<p>Sua fatura está disponível. Confira os detalhes e escolha a forma de pagamento:</p>
<div class="info-box">
<p><strong>Plano:</strong> ${item.plano}</p>
<p><strong>Valor:</strong> ${item.valorFormatado}</p>
<p><strong>Vencimento:</strong> ${item.dataVencimento}</p>
</div>
<div class="btn-wrap">
<a href="${item.linkBoleto}" class="btn">📄 Pagar com Boleto</a>
<a href="${item.linkPix}" class="btn btn-pix">⚡ Pagar com PIX</a>
</div>
<p style="font-size:13px;color:#666;">Os links expiram na data de vencimento.</p>
</div>
<div class="footer">Este é um e-mail automático. Por favor, não responda diretamente.</div>
</div>
</body>
</html>`;
return [{ json: { ...item, htmlBody } }];
14. Enviar E-mail
Tipo: Gmail
Operação: Send
Envia o e-mail com o HTML montado no nó anterior.
Configuração:
| Campo | Valor |
|---|---|
| To | ={{ $json.email }} |
| Subject | ={{ $json.subject }} |
| Email Type | HTML |
| Message | ={{ $json.htmlBody }} |
!!! warning "Expressões no Gmail"
O campo Message do nó Gmail não interpreta {{ }} dentro de strings HTML. Todo o HTML deve ser montado previamente em um nó Code e passado como uma variável única via ={{ $json.htmlBody }}.
!!! info "Limite do Gmail" Contas Gmail pessoais: 500 e-mails/dia. Google Workspace: 2.000 e-mails/dia.
15. Aguardar 1 minuto
Tipo: Wait
Duração: 1 minuto
Aguarda 1 minuto entre cada cliente para evitar sobrecarga nas APIs e limites de envio do Gmail.
Após o wait, o fluxo retorna para o nó Loop que processa o próximo cliente.
Erros Conhecidos e Soluções
Sheet with ID Clientes not found
O nome da aba no campo sheetName não corresponde ao nome real. Verifique maiúsculas/minúsculas e espaços.
The 'Column to Match On' parameter is required
No nó Google Sheets com operação Update, o campo matchingColumns é obrigatório. Configure como row_number.
Invalid email address
O campo sendTo do Gmail está recebendo a expressão como texto literal. Certifique-se de que o ícone = está ativo (azul) no campo, não o ícone T (texto fixo).
O campo dueDate deve ser informado / O campo value deve ser informado
Os campos nos nós HTTP Request estão em modo texto. Ative o = em cada campo ou use um nó Code para montar o body manualmente.
Loop para no nó Unificar Customer ID
O nó Merge em modo Choose Branch com Output Type: Wait for All Inputs to Arrive trava esperando as duas entradas. Solução: eliminar o Merge e referenciar o Customer ID via expressão condicional diretamente nos nós de criação de cobrança.
bankSlipUrl é null
Comportamento normal do sandbox Asaas. Use invoiceUrl para ambos os botões. Em produção o bankSlipUrl é preenchido.
Expressões {{ }} aparecem como texto no e-mail
O HTML do e-mail foi montado com {{ $json.campo }} dentro de uma string no nó Set ou Gmail. Mova a montagem do HTML para um nó Code e use template literals do JavaScript (${variavel}).
Passagem para Produção
-
Substitua a URL base do Asaas:
- Sandbox:
https://sandbox.asaas.com/api/v3 - Produção:
https://api.asaas.com/api/v3
- Sandbox:
-
Troque a credencial para a API key de produção
-
Verifique se a coluna
Asaas Customer IDestá vazia para os clientes novos (em produção os IDs do sandbox não são válidos) -
Ative o workflow no n8n (toggle Active no topo do canvas)
Estrutura de Arquivos
workflow/
└── cobranca_email_workflow.json # Importar no n8n via Workflows → Import from File