Dedupe a Google Sheet with Apps Script

For a one-off, use Data → Data cleanup or the Sheets guide. This page is the automated version: a thirty-line Apps Script that adds a Dedupe.dev menu to your sheet and runs exact or fuzzy matching through the API, so recurring cleanups become one click, or a time-driven trigger with no clicks at all.

Setup, once

  1. Create a free API key at /app.
  2. In your sheet: Extensions → Apps Script, paste the script below, save.
  3. In the script editor: Project Settings → Script Properties, add DEDUPE_KEY with your key.
  4. Reload the sheet. A Dedupe.dev menu appears.

The script

const FUZZY = false;      // true for fuzzy name matching
const THRESHOLD = 0.88;   // used only when FUZZY is true
const KEY_COLUMNS = [];   // zero-based, e.g. [0] for column A; [] = whole row

function onOpen() {
  SpreadsheetApp.getUi().createMenu("Dedupe.dev")
    .addItem("Remove duplicate rows", "dedupeActiveSheet")
    .addToUi();
}

function dedupeActiveSheet() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getDisplayValues();
  const csv = rows.map(r => r.map(cell => {
    const s = String(cell);
    return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  }).join(",")).join("\n");

  const key = PropertiesService.getScriptProperties().getProperty("DEDUPE_KEY");
  const res = UrlFetchApp.fetch("https://dedupe.dev/api/dedupe", {
    method: "post",
    contentType: "application/json",
    headers: { Authorization: "Bearer " + key },
    payload: JSON.stringify({
      mode: "rows",
      data: csv,
      options: { keyColumns: KEY_COLUMNS, hasHeader: true,
                 fuzzy: FUZZY, threshold: FUZZY ? THRESHOLD : undefined },
    }),
    muteHttpExceptions: true,
  });
  const body = JSON.parse(res.getContentText());
  if (res.getResponseCode() !== 200) throw new Error(body.error);

  const cleaned = Utilities.parseCsv(body.result);
  sheet.clearContents();
  sheet.getRange(1, 1, cleaned.length, cleaned[0].length).setValues(cleaned);
  SpreadsheetApp.getActiveSpreadsheet().toast(
    body.kept + " rows kept, " + body.removed + " duplicates removed", "Dedupe.dev");
}

What to know before running it

The script replaces the sheet's contents with the deduplicated rows, so run it on a copy the first time. Rows travel to the API over HTTPS and are processed in memory, never stored; if the sheet must not leave your machine at all, use the browser tool on a CSV download instead, which is free and uploads nothing. Each run is metered like any API call (one unit per row), and appears in your dashboard with its row counts. To run it nightly with no clicks, add a time-driven trigger in the script editor pointing at dedupeActiveSheet.

FAQ

Frequently asked questions

Does this upload my whole spreadsheet?

It sends the active sheet's rows to the dedupe API over HTTPS, which is the point of the script: this is the API path, not the local browser tool. Nothing sent is stored; the endpoint deduplicates in memory and answers. For the zero-upload version, download the sheet as CSV and use the in-browser tool instead.

Where does the API key live?

In Script Properties (Project Settings → Script Properties), not in the code. Anyone who can open the script editor can read properties, so on a shared sheet treat the key as shared with your collaborators, and rotate it from /app/keys if that changes.

Can it do fuzzy matching?

Yes: set FUZZY to true and pick a threshold in the script. 0.85 to 0.92 works for names. The run replaces the sheet contents with the deduplicated rows, so check the removed count in the toast before saving anything downstream.

Why not a marketplace add-on?

An add-on needs OAuth scopes reviewed by Google and gives us standing access to your spreadsheets. Thirty lines of script you paste yourself do the same job with access you can read line by line.

Last updated