Extract contacts from any webpage and send them anywhere — Google Sheets, MongoDB, or Go High Level.
Install the Extension in Chrome
Do this once. After install, the extension icon appears in your Chrome toolbar.
.zip → Extract All → choose a permanent folder (don't delete it).chrome://extensions and press Enter. Toggle Developer mode on (top-right switch).dist folder inside it → click Select Folder.Step 2 — Choose where to send contacts
Google Sheets Setup
No backend or API key needed. Contacts are sent directly from the extension to your Google Sheet using a Web App URL you deploy yourself. Takes about 5 minutes.
function clearAllData() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.getRange(2, 1, lastRow - 1, sheet.getMaxColumns()).clearContent();
}
}
function doPost(e) {
try {
var lock = LockService.getScriptLock();
lock.waitLock(30000);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var payload = JSON.parse(e.postData.contents);
if (payload.action === "clear") {
clearAllData();
lock.releaseLock();
return ContentService
.createTextOutput(JSON.stringify({ status: "cleared" }))
.setMimeType(ContentService.MimeType.JSON);
}
var numCols = sheet.getLastColumn() || 9;
var headerValues = sheet.getRange(1, 1, 1, numCols).getValues()[0];
if (!headerValues[0]) {
var defaultHeaders = ["Name","Phone (Primary)","Phone (Secondary)",
"Email (Primary)","Email (Secondary)","Company","Website","Profile URL","Note"];
sheet.getRange(1, 1, 1, defaultHeaders.length).setValues([defaultHeaders]);
headerValues = defaultHeaders;
numCols = defaultHeaders.length;
}
var contacts = Array.isArray(payload) ? payload : (payload.contacts || [payload]);
// Dedup by email: skip contacts already in the sheet
var emailColIdx = -1;
for (var j = 0; j < headerValues.length; j++) {
var hh = (headerValues[j] || '').toString().toLowerCase().trim();
if (hh.indexOf('email') !== -1 && hh.indexOf('secondary') === -1) { emailColIdx = j; break; }
}
var existingEmails = {};
if (emailColIdx >= 0 && sheet.getLastRow() > 1) {
var emailCol = sheet.getRange(2, emailColIdx + 1, sheet.getLastRow() - 1, 1).getValues();
for (var ei = 0; ei < emailCol.length; ei++) {
var ev = emailCol[ei][0];
if (ev) existingEmails[ev.toString().toLowerCase().trim()] = true;
}
}
contacts = contacts.filter(function(c) {
return !c.email || !existingEmails[c.email.toLowerCase().trim()];
});
// Find first empty row in column A
var colA = sheet.getRange("A:A").getValues();
var startRow = 2;
for (var r = 1; r < colA.length; r++) {
if (!colA[r][0] || colA[r][0].toString().trim() === "") { startRow = r + 1; break; }
startRow = r + 2;
}
var rows = [];
for (var i = 0; i < contacts.length; i++) {
var c = contacts[i];
var row = [];
for (var jj = 0; jj < headerValues.length; jj++) {
var h = (headerValues[jj] || "").toString().toLowerCase().trim();
if (h.indexOf("name") !== -1) row.push(c.name || "");
else if (h.indexOf("phone") !== -1 && h.indexOf("secondary") !== -1) row.push(c.phoneSecondary || "");
else if (h.indexOf("phone") !== -1) row.push(c.phone || "");
else if (h.indexOf("email") !== -1 && h.indexOf("secondary") !== -1) row.push(c.emailSecondary || "");
else if (h.indexOf("email") !== -1) row.push(c.email || "");
else if (h.indexOf("company") !== -1) row.push(c.company || "");
else if (h.indexOf("website") !== -1) row.push(c.website || "");
else if (h.indexOf("profile") !== -1) row.push(c.profileUrl || "");
else if (h.indexOf("note") !== -1) {
var noteParts = [];
if (c.jobTitle) noteParts.push(c.jobTitle);
if (c.location) noteParts.push(c.location);
row.push(noteParts.join(" | "));
} else row.push("");
}
rows.push(row);
}
if (rows.length > 0) {
sheet.getRange(startRow, 1, rows.length, headerValues.length).setValues(rows);
}
lock.releaseLock();
return ContentService
.createTextOutput(JSON.stringify({ status: "success", count: rows.length }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({ status: "error", message: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doGet(e) {
if (e && e.parameter && e.parameter.action === "clear") {
clearAllData();
return ContentService
.createTextOutput(JSON.stringify({ status: "cleared" }))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService
.createTextOutput(JSON.stringify({ status: "ok", message: "Grapper webhook is active!" }))
.setMimeType(ContentService.MimeType.JSON);
}
https://script.google.com/macros/s/AKfycb.../execWhat gets created in your sheet
MongoDB via Grapper Backend
The Grapper backend stores contacts in MongoDB (Atlas), gives you an API to query them, and securely proxies your GHL credentials. You need the backend URL and an API key from the person who set up the server.
https://grapper.yourdomain.com)npm run create-key in the backend/ folder. The plaintext key is shown once — copy it and send it securely to your user.
What the backend stores
GET /api/contacts?q=name&domain=example.comGo High Level (GHL) Setup
Send contacts directly into a GHL sub-account. Requires a Private Integration Token (PIT) from your GHL account. The token is encrypted with AES-256 on the backend — it is never stored in your browser.
••••••••••••abcd) — the raw token is never sent back to your browser again.
What gets sent to GHL
linkedin_profilejob_titlelocationcompany_website
Chrome only · Questions? Check the README or contact whoever shared this link.