Pular para conteúdo

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)

  1. No n8n, vá em Settings → Credentials → Add Credential
  2. Selecione Google Sheets OAuth2 API
  3. Autentique com sua conta Google
  4. Repita o processo para Gmail OAuth2

Asaas API Key (HTTP Header Auth)

  1. No Asaas Sandbox: Minha Conta → Integrações → API Key
  2. Copie a chave
  3. No n8n, crie uma credencial do tipo HTTP Header Auth:
    • Name: Asaas API Key
    • Header Name: access_token
    • Header Value: sua_api_key_aqui

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 minuto e 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.


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">&#128196; Pagar com Boleto</a>
        <a href="${item.linkPix}" class="btn btn-pix">&#9889; 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

  1. Substitua a URL base do Asaas:

    • Sandbox: https://sandbox.asaas.com/api/v3
    • Produção: https://api.asaas.com/api/v3
  2. Troque a credencial para a API key de produção

  3. Verifique se a coluna Asaas Customer ID está vazia para os clientes novos (em produção os IDs do sandbox não são válidos)

  4. 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

Referências