Documentation Integrations

Export Leads to spreadsheets

This guide takes you from "I have Leads on my Node" to "my spreadsheet refreshes on its own", in Excel (Power Query) and in Google Sheets (Apps Script).

It is written for two readers: the operator, who creates the source in the Console, and the analyst, who builds the spreadsheet. You do not need to know HTTP, how a CSV works inside, or how to program Apps Script: the script is ready, further down.

What the export is

  • A CSV file on your Node. Each source publishes one file at a URL on your own Node, protected by a username and a password of its own.
  • The spreadsheet comes to fetch it. The Node sends the file nowhere: Excel or Google Sheets fetches the URL when it refreshes.
  • One row per Lead, only from the Property the source was created in. Test (sandbox) Leads never go in.
  • Nothing passes through Supreme's servers. Excel fetches the file straight from your Node. In Google Sheets, Google's servers fetch your Node's URL over the internet.

You need two things:

  • the Leads power-up on your plan, to create a source and edit its columns;
  • the Spreadsheet/CSV Export module installed on your Node, under Integrations. See how to install modules in Integrations overview.

Create the source

  1. In the Console, open Leads and pick the Property in the Property selector.
  2. Click How to capture Leads and open the Spreadsheet tab. While the Property has no Lead yet, the capture paths are already on screen, without that button.
  3. Read the notice above the panel: the spreadsheet carries your Leads' name, email and phone.
  4. Click New source (the first time, Create spreadsheet source).
  5. Tick the columns and pick the filters: the next section explains each one. The name is optional and only helps you tell sources apart.
  6. Click Create source.
  7. Copy the Source URL, the Username and the Password.

Copy the URL exactly as the Console shows it; do not build the address by hand. You can see the password again later with Show password. If the Node can no longer read the stored password, the Console says so, and the way out is a New password.

If the module is not installed, the Spreadsheet tab shows "Spreadsheet/CSV Export module not installed." and the Open Integrations button.

Without the Leads power-up, New source is disabled, with the way to See plans. For someone who does not own the Node, the button is disabled with the line "Ask the Node owner to turn Leads on." A source that already exists keeps serving its file as usual, and the Console keeps it listed with its URL, Pause, New password and Delete.

Columns and filters

The columns always come out in this order, with these names in the header, in English. Power Query and the Google Sheets script rely on them, so they do not change.

Column in the file Field in the Console Default
lead_id always included, cannot be unticked always
first_name, last_name Nome (Name) ticked
email E-mail (Email) ticked
phone Telefone (Phone) ticked
status Status ticked
source Origem (Source) ticked
source_lead_id ID do lead na origem (Source lead ID) unticked
value, currency Valor e moeda (Value and currency) unticked
created_at Criado em (Created at) ticked
updated_at Atualizado em (Updated at) ticked
utm_source, utm_medium, utm_campaign, utm_content, utm_term UTMs ticked
custom_fields Campos personalizados (Custom fields) unticked

The field names come from the module itself, which writes them in Portuguese, so they appear in Portuguese even when the Console is in English; the translation is in parentheses. On some fields, the text next to the box says which columns it turns on, such as "Colunas first_name e last_name" (first_name and last_name columns) on Nome.

  • lead_id is the Lead's identifier on your Node.
  • source_lead_id is only a reference to the system the Lead came from, such as its id in a CRM. It is never the person's identity.
  • created_at is the moment the Lead was created on your Node; updated_at, the last time it changed.
  • The UTMs are the first-touch ones, written once, when the Lead was created.

The filters:

  • Filtro de status (Status filter): Todos (All), or a single status: new, contacted, qualified, won or lost.
  • Criado a partir de (UTC, inclusivo) (Created from, UTC, inclusive) and Criado antes de (UTC, exclusivo) (Created before, UTC, exclusive): a date in the YYYY-MM-DD format, which the field shows as AAAA-MM-DD, its Portuguese spelling (for example, 2026-07-01). Empty means no limit. Each date starts at 00:00 UTC.

A filter outside these formats makes the source answer with an error (code 500) on every read, until it is fixed in the Console.

Edit columns, on the source card, changes only the selection of columns and filters: adjust it and click Save columns. The URL, the username and the password stay the same, and the spreadsheet keeps working. After saving:

  • In Google Sheets, nothing to do: the script rewrites the whole tab on every refresh. Just remember that a formula on another tab that points at a column of the Leads tab by its letter now reads whatever sits under that letter.
  • In Excel, the query suggested in the Excel section keeps working. If your query has a step that names a column, such as a type step, removing that column makes the next refresh fail with a column-not-found error: delete or adjust that step.

The file format

The file is a CSV following RFC 4180:

  • fields separated by commas and lines ending in CRLF (\r\n);
  • a value with a comma, a quote or a line break comes inside quotes, with inner quotes doubled;
  • UTF-8 encoding, no BOM;
  • dates and times in ISO-8601, in UTC: 2026-09-23T12:00:00Z;
  • value with a decimal point and two decimals (150.50), and the currency in the next column, currency (BRL);
  • the whole custom_fields in a single cell, as a JSON object;
  • an empty cell means the Lead has no such value.

Name and email arrive in lower case, and the phone arrives as digits only (5511987654321, without the +): that is how your Node stores the person's identity, to recognise them on the next capture.

A file with the default columns starts like this:

lead_id,first_name,last_name,email,phone,status,source,created_at,updated_at,utm_source,utm_medium,utm_campaign,utm_content,utm_term
ld_7f3a9c2e5b1d4f6a8c0e2b4d,maria,silva,maria@example.com,5511987654321,qualified,landing-page,2026-09-23T12:00:00Z,2026-09-24T09:30:00Z,instagram,social,lancamento,'@perfil-parceiro,
ld_1b2c3d4e5f60718293a4b5c6,joão,"souza, filho",joao@example.com,,new,website,2026-09-24T15:42:10Z,2026-09-24T15:42:10Z,,,,,

Formula protection. Any text that starts with =, +, -, @, TAB or CR arrives with a ' in front. In the example above, the UTM @perfil-parceiro arrived as '@perfil-parceiro. It is protection, not a defect: without it, a value typed into a form on your site could turn into a formula inside your spreadsheet. value never gets the '.

What you see in each spreadsheet:

  • Excel (Power Query): the ' shows in the cell, as a character of the text ('@perfil-parceiro).
  • Google Sheets: the ' makes the cell keep the value as text and does not show in it (@perfil-parceiro).

Ceilings and how to split a large base

Each read of the source has three ceilings, and whichever comes first applies:

  • 10,000 lines counting the header, that is, up to 9,999 Leads;
  • 16 MB;
  • 20 seconds to build the file.

Above that, the file arrives cut. It ends on a whole line, so the spreadsheet notices nothing. Because the file goes from the oldest Lead to the newest, what is left out is the most recent Leads. The Console is what tells you: the source card shows Served, truncated and the notice "File cut at the lane ceiling — split the source by status or period." A cut file is never a complete export.

Split without overlap

Pick a date D and create two sources:

  • one with Criado antes de (Created before) = D;
  • another with Criado a partir de (Created from) = D.

Together they cover every Lead without repeating any: the first stops at 00:00 UTC on D, and the second starts exactly there. For more parts, repeat with other dates. created_at does not change after the Lead is created, so each Lead always stays in the same part.

Another way out is one source per status. In that case, the split follows the status at the moment of the read: a Lead that changes status leaves one source and enters the other.

Read window

Each source accepts 60 reads every 5 minutes. The 61st is refused with code 429, with how many seconds to wait until the window reopens. The window belongs to the source, not to each computer: every spreadsheet and person reading the same source share the same 60 reads. Refreshing hourly stays far below that.

Excel (Power Query) on Windows

The steps are for Excel for Microsoft 365 on Windows. Menu names are in English, with the Portuguese interface name in parentheses.

  1. In Data → Get Data → From Other Sources → From Web (Dados → Obter Dados → De Outras Fontes → Da Web), leave Basic (Básico) selected, paste the Source URL and click OK.
  2. In the credentials window, choose Basic (Básico) on the left. In User name (Nome de usuário), paste the Console's Username; in Password (Senha), the Password. For the level to apply these settings to, choose the source's full URL, not the site root: each source has its own username and password. Click Connect (Conectar).
  3. In the preview, check File Origin (Origem do Arquivo) = 65001: Unicode (UTF-8) and Delimiter (Delimitador) = Comma (Vírgula). Click Transform Data (Transformar Dados), check that the first row became the header (lead_id, first_name…) and click Close & Load (Fechar e Carregar).
  4. Or, instead of steps 1 and 3, use this query. It is the recommended way when Valor e moeda (Value and currency) is ticked. Create a Blank Query (Consulta Nula) under Data → Get Data → From Other Sources, open the Advanced Editor (Editor Avançado) and paste the text below, replacing the example address with the Source URL copied from the Console, in full. On the first refresh, Excel asks for the credential: follow step 2.
let
    Source = Csv.Document(
        Web.Contents("https://YOUR-NODE/public/data/spreadsheet_export/<USERNAME>/leads.csv"),
        [Delimiter = ",", Encoding = 65001, QuoteStyle = QuoteStyle.Csv]
    ),
    Headers = Table.PromoteHeaders(Source, [PromoteAllScalars = true]),
    Leads = if List.Contains(Table.ColumnNames(Headers), "value")
        then Table.TransformColumnTypes(Headers, {{"value", type number}}, "en-US")
        else Headers
in
    Leads

QuoteStyle.Csv is what keeps a line break inside a quoted value. The Leads step converts value with the English (United States) (Inglês (Estados Unidos)) locale: in an Excel set to Portuguese, 150.50 read without that locale turns into a different number. The query names value only, and only when the column exists, so it keeps working when you tick or untick columns with Edit columns.

  1. To refresh on its own: in Data → Queries & Connections (Dados → Consultas e Conexões), right-click the query, open Properties (Propriedades) and tick "Refresh every N minutes" ("Atualizar a cada N minutos") and "Refresh data when opening the file" ("Atualizar dados ao abrir o arquivo"). Hourly is 60 minutes. Excel only refreshes while the file is open.
  2. The password lives in that computer's data source settings, not inside the workbook: whoever opens the file on another computer enters the username and password again. To change the password, go to Data → Get Data → Data Source Settings (Dados → Obter Dados → Configurações da Fonte de Dados), select the URL, click Edit Permissions (Editar Permissões) and, under Credentials (Credenciais), click Edit (Editar).

Excel for Mac and Excel on the web have not been verified on this page.

Google Sheets (Apps Script)

A Google Sheets formula cannot fetch the source: the reason is at the end of this page. A short script, which you paste once into the spreadsheet, fetches with the username and password and rewrites a tab called Leads.

  1. In the spreadsheet, open Extensions → Apps Script (Extensões → Apps Script).
  2. Delete the contents of Code.gs (Código.gs), paste the script below in full and save.
  3. Reload the spreadsheet: the Supreme menu appears.
  4. In Supreme → Configurar fonte / Set up source, paste the Console's URL, username and password. The password field does not hide what you type.
  5. The first time, Google asks for authorization. The unverified-app warning is normal for your own script: click Advanced (Avançado) and then Go to… (Acessar…). If the menu item does not run after you authorize, click it again.
  6. Supreme → Atualizar agora / Refresh now creates or rewrites the Leads tab with the file.
  7. Supreme → Atualizar a cada hora / Refresh every hour installs the time-driven trigger. Supreme → Parar atualização / Stop refreshing removes that trigger.

The menu items are bilingual, Portuguese first, exactly as the script writes them.

// Supreme Tracking — Leads no Google Sheets / Leads in Google Sheets
// Cole o arquivo inteiro em Extensões > Apps Script e recarregue a planilha.
// Paste the whole file into Extensions > Apps Script and reload the spreadsheet.
// Nenhuma URL, usuário ou senha fica no código: use Supreme > Configurar fonte.
// No URL, username or password lives in the code: use Supreme > Set up source.

var SHEET_NAME = 'Leads';
// Horas entre atualizações: 1, 2, 4, 6, 8 ou 12. / Hours between refreshes: 1, 2, 4, 6, 8 or 12.
var REFRESH_HOURS = 1;

var KEY_URL = 'SUPREME_LEADS_URL';
var KEY_USER = 'SUPREME_LEADS_USER';
var KEY_PASSWORD = 'SUPREME_LEADS_PASSWORD';

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Supreme')
    .addItem('Configurar fonte / Set up source', 'supremeSetup')
    .addItem('Atualizar agora / Refresh now', 'supremeRefreshLeads')
    .addItem('Atualizar a cada hora / Refresh every hour', 'supremeInstallTrigger')
    .addItem('Parar atualização / Stop refreshing', 'supremeRemoveTriggers')
    .addToUi();
}

// Guarda URL, usuário e senha só para você. / Stores URL, username and password for you only.
function supremeSetup() {
  var ui = SpreadsheetApp.getUi();
  var url = supremeAsk_(ui, 'URL da fonte / Source URL',
    'Cole a URL da fonte copiada do Console. / Paste the source URL copied from the Console.');
  if (url === null) return;
  if (url.indexOf('https://') !== 0) {
    ui.alert('A URL precisa começar com https://. Nada foi salvo. / The URL must start with https://. Nothing was saved.');
    return;
  }
  var user = supremeAsk_(ui, 'Usuário / Username',
    'Cole o usuário da fonte. / Paste the source username.');
  if (user === null) return;
  var password = supremeAsk_(ui, 'Senha / Password',
    'Cole a senha da fonte. Atenção: este campo não esconde o texto. / Paste the source password. Note: this field does not hide the text.');
  if (password === null) return;

  var values = {};
  values[KEY_URL] = url;
  values[KEY_USER] = user;
  values[KEY_PASSWORD] = password;
  PropertiesService.getUserProperties().setProperties(values);
  ui.alert('Fonte salva só para você. Agora use Supreme > Atualizar agora. / Source saved for you only. Now use Supreme > Refresh now.');
}

function supremeAsk_(ui, title, message) {
  var response = ui.prompt(title, message, ui.ButtonSet.OK_CANCEL);
  var value = response.getResponseText().trim();
  if (response.getSelectedButton() != ui.Button.OK || value === '') {
    ui.alert('Cancelado. Nada foi salvo. / Cancelled. Nothing was saved.');
    return null;
  }
  return value;
}

// Busca o CSV com Basic e reescreve a aba; em erro, a aba fica como estava.
// Fetches the CSV with Basic auth and rewrites the tab; on error, the tab stays as it was.
function supremeRefreshLeads() {
  var props = PropertiesService.getUserProperties();
  var url = props.getProperty(KEY_URL);
  var user = props.getProperty(KEY_USER);
  var password = props.getProperty(KEY_PASSWORD);
  if (!url || !user || !password) {
    throw new Error('Rode Supreme > Configurar fonte primeiro. / Run Supreme > Set up source first.');
  }

  var response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      Authorization: 'Basic ' + Utilities.base64Encode(user + ':' + password, Utilities.Charset.UTF_8)
    },
    muteHttpExceptions: true,
    followRedirects: false
  });

  var code = response.getResponseCode();
  if (code !== 200) {
    throw new Error(supremeErrorMessage_(code, response.getHeaders()));
  }

  var rows = Utilities.parseCsv(response.getContentText('UTF-8')).filter(function (row) {
    return row.join('') !== '';
  });
  if (rows.length === 0) {
    throw new Error('A fonte devolveu uma resposta vazia. / The source returned an empty response.');
  }
  var width = rows[0].length;
  if (rows.some(function (row) { return row.length !== width; })) {
    throw new Error('Resposta inesperada do Node: linhas de tamanhos diferentes. / Unexpected response from the Node: rows of different lengths.');
  }

  // Só a coluna value vira número. / Only the value column becomes a number.
  var valueColumn = rows[0].indexOf('value');
  if (valueColumn !== -1) {
    for (var r = 1; r < rows.length; r++) {
      if (/^\d+\.\d{2}$/.test(rows[r][valueColumn])) {
        rows[r][valueColumn] = Number(rows[r][valueColumn]);
      }
    }
  }

  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName(SHEET_NAME) || spreadsheet.insertSheet(SHEET_NAME);
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, width).setValues(rows);
}

function supremeErrorMessage_(code, headers) {
  if (code === 401) {
    return 'Usuário ou senha recusados (401). A senha foi trocada ou a fonte foi excluída? Rode Supreme > Configurar fonte com os dados atuais do Console. / Username or password refused (401). Was the password changed or the source deleted? Run Supreme > Set up source with the current details from the Console.';
  }
  if (code === 429) {
    var wait = headers['Retry-After'] || headers['retry-after'] || '?';
    return 'Limite de leituras da fonte atingido (429). Tente de novo em ' + wait + ' s. / Source read limit reached (429). Try again in ' + wait + ' s.';
  }
  if (code === 503) {
    return 'Fonte pausada ou Propriedade inativa (503). Retome a fonte ou ative a Propriedade no Console. / Source paused or Property inactive (503). Resume the source or activate the Property in the Console.';
  }
  if (code === 404) {
    return 'URL errada ou módulo desativado no Node (404). Copie a URL de novo do Console. / Wrong URL or module turned off on the Node (404). Copy the URL again from the Console.';
  }
  if (code === 500) {
    return 'Erro no Node (500): a configuração da fonte pode estar inválida. Revise colunas e filtros no Console. / Node error (500): the source configuration may be invalid. Review columns and filters in the Console.';
  }
  if (code >= 300 && code < 400) {
    return 'A URL redireciona (' + code + '). Cole a URL https final, como o Console mostra. / The URL redirects (' + code + '). Paste the final https URL, as the Console shows it.';
  }
  return 'Resposta inesperada do Node (' + code + '). / Unexpected response from the Node (' + code + ').';
}

// Instala um gatilho de tempo, sem duplicar. / Installs one time-driven trigger, never two.
function supremeInstallTrigger() {
  supremeDeleteRefreshTriggers_();
  ScriptApp.newTrigger('supremeRefreshLeads').timeBased().everyHours(REFRESH_HOURS).create();
  SpreadsheetApp.getUi().alert('Atualização automática ligada: a cada ' + REFRESH_HOURS + ' h. / Automatic refresh on: every ' + REFRESH_HOURS + ' h.');
}

function supremeRemoveTriggers() {
  var removed = supremeDeleteRefreshTriggers_();
  SpreadsheetApp.getUi().alert('Gatilhos removidos: ' + removed + '. / Triggers removed: ' + removed + '.');
}

// Apaga só os gatilhos de supremeRefreshLeads. / Deletes only the supremeRefreshLeads triggers.
function supremeDeleteRefreshTriggers_() {
  var removed = 0;
  ScriptApp.getProjectTriggers().forEach(function (trigger) {
    if (trigger.getHandlerFunction() === 'supremeRefreshLeads') {
      ScriptApp.deleteTrigger(trigger);
      removed++;
    }
  });
  return removed;
}

What the script does, and what it does not:

  • The URL, username and password live in your user properties for the script: never in a cell, never in the code.
  • On every refresh the Leads tab is rewritten in full, identical to the file, so do not type anything in it. If you already use a tab with that name, change SHEET_NAME at the top of the script.
  • On error, the tab is not touched: the last good data stays, and the message says what happened based on the response code.
  • Only the value column becomes a number, so a spreadsheet set to Portuguese does not reinterpret the point in 150.50. The script converts nothing else.
  • For another interval, change REFRESH_HOURS (1, 2, 4, 6, 8 or 12) and click Refresh every hour again: the old trigger is replaced by the new one, never added to it.
  • The trigger runs as the person who installed it: their credential is the one used, and trigger failures reach them by email from Google. Someone else who clicks Refresh now needs to run their own Set up source, and Stop refreshing only removes the trigger of whoever clicks it.

Check that it worked

  1. Refresh the spreadsheet.
  2. In the Console, look at the source card: Last fetch shows the time of the read, File served confirms the file was not cut, and Rows served says how many lines went out, counting the header.
  3. The spreadsheet should have the same number of lines: Rows served 101 means the header and 100 Leads.

This proves your Node served the whole file and the spreadsheet received every line. The Console does not know what the spreadsheet did with the file afterwards: formulas, filters and pivot tables live on the spreadsheet side. A test (sandbox) Lead never shows up; to see a new row, send a real form and refresh.

New password, pause and deletion

  • New password: the old password stops working immediately. Excel refuses the refresh and asks for the credential again; the Google Sheets script fails with the message for code 401. Update the password (in Excel, through step 6 of the Excel section; in Google Sheets, by running Set up source again) and the next refresh works again, with the same URL and the same username. The change also resets the source's read window.
  • Pause: the URL and the password stay the same. While the source is paused it answers with code 503 and nothing is read; the script shows the paused-source message. Resume brings it back to normal.
  • Delete: the URL is gone for good, and whoever tries to read it gets the same 401 as a wrong password. Recreating the source produces another URL, another username and another password, and every spreadsheet has to be connected again. To only stop serving the data, use Pause.

Share safely

The spreadsheet holds PII: your Leads' name, email and phone. Whoever has the spreadsheet sees that data.

  • Share it only with whoever needs it, and prefer viewer access.
  • Google Sheets' version history keeps the old data: deleting a row today does not delete what stayed in the history.
  • A copy of an Excel file carries along the data already loaded into it.
  • The password is not inside the file: in Excel it lives in the computer's data source settings; in Google Sheets, in the user properties of whoever ran Set up source.
  • In Google Sheets, however, whoever can edit the spreadsheet can edit the script, and a modified script runs with the credential of whoever installed the trigger. Give edit access only to people you trust.
  • When someone with access leaves, remove their access to the spreadsheet and use New password.

Why not =IMPORTDATA(), and why the password never goes in the URL

IMPORTDATA() only takes a URL. It does not send the Authorization: Basic header, which is how the source receives the username and the password, so the answer is 401 and no Lead arrives.

The source also does not accept the username and password as URL parameters (the query string), on purpose. URLs end up in browser history, in server and proxy logs, in screenshots and in shared links: a password in the URL would make the PII file, in practice, public.

Sending that header is exactly what the script in the Google Sheets section exists for.

When something does not work

What you see What to do
Excel asks for the username and password again, or the script mentions 401 The password was changed, the source was deleted or the credential is wrong. Copy the password from the Console (Show password) and update it: in Excel, step 6; in Google Sheets, Set up source.
"Run Supreme > Set up source first." Whoever clicked has not set up the source yet. Each person runs their own Set up source.
Code 429, read limit Wait the seconds shown and refresh less often: it is 60 reads per source every 5 minutes, for everyone together.
Code 503 The source is paused or the Property is inactive. Use Resume, or activate the Property.
Code 404 The URL is wrong or incomplete, or the module was turned off on the Node. Copy the URL again from the Console and check the module under Integrations.
Code 500 A filter is invalid, usually a date outside the YYYY-MM-DD format. Fix it with Edit columns.
Fewer Leads in the spreadsheet than you expected Check the source's filters and whether the card shows Served, truncated. If it does, split the source.
value with the wrong number in Excel Use the query from step 4 of the Excel section, which converts value with the English (United States) locale.
A ' before a value That is the formula protection.
Name in lower case, phone without + That is how your Node stores the person's identity.

Next steps