Grapper Setup Guide

Extract contacts from any webpage and send them anywhere — Google Sheets, MongoDB, or Go High Level.

Step 1 — Download & Install the Extension

Chrome only  ·  Takes under 2 minutes

Download Extension (.zip)

Install the Extension in Chrome

Do this once. After install, the extension icon appears in your Chrome toolbar.

  1. 1
    Download & unzip
    Click the button above. Once downloaded, right-click the .zip → Extract All → choose a permanent folder (don't delete it).
  2. 2
    Open Chrome Extensions
    In Chrome address bar type chrome://extensions and press Enter. Toggle Developer mode on (top-right switch).
  3. 3
    Load the extension folder
    Click Load unpacked → navigate to the unzipped folder → select the dist folder inside it → click Select Folder.
  4. 4
    Pin it to your toolbar
    Click the puzzle-piece icon in Chrome's toolbar → find Grapper → click the pin icon. The Grapper icon will now show in your toolbar.

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.

  1. 1
    Open (or create) your Google Sheet
    Go to sheets.google.com and open the spreadsheet where you want contacts to land. Any sheet works — Grapper will create the column headers automatically on first use.
  2. 2
    Open Apps Script
    In the sheet menu bar click Extensions → Apps Script. A new browser tab opens with the script editor.
  3. 3
    Paste the Grapper script
    Select all existing code in the editor (Ctrl+A) and delete it. Then paste the code below and save with Ctrl+S.
    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);
    }
  4. 4
    Deploy as a Web App
    In the Apps Script editor, click Deploy (top-right) → New deployment.

    In the dialog that opens:
    • Click the gear icon next to Select type → choose Web app
    • Description: Grapper webhook (anything)
    • Execute as: Me
    • Who has access: Anyone ← important!
    Click Deploy. Google will ask you to authorise — click Authorize access, sign in with your Google account, and click Allow.
  5. 5
    Copy the Web App URL
    After deploying, you'll see a Web app URL that looks like:
    https://script.google.com/macros/s/AKfycb.../exec

    Copy this entire URL.
  6. 6
    Paste it into the Extension Settings
    Click the Grapper icon in your Chrome toolbar → go to Settings tab → find Google Apps Script Webhook URL → paste the URL → click Save Settings.
  7. 7
    Test it
    Open any LinkedIn profile → click Scan Page → tick the contact → click Send to Sheet. Check your Google Sheet — the contact should appear in Row 2 with headers auto-created in Row 1.
✓ Already done? If you need to re-deploy after updating the script, go to Deploy → Manage deployments → pencil icon → New version → Deploy. The URL stays the same.
ℹ Duplicate protection: The script checks the email column before inserting — contacts with an email already in the sheet are automatically skipped.

  • Row 1 — headers auto-created on first send: Name, Phone, Email, Company, Website, Profile URL, Note
  • Each contact gets one row starting at Row 2
  • Note column combines Job Title and Location (e.g. "CEO | New York")
  • You can rename or reorder columns — Grapper reads the header row each time

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.

  1. 1
    Get your API URL and API Key
    Ask the backend administrator for:
    • The API URL (e.g. https://grapper.yourdomain.com)
    • Your personal API Key (a long hex string)
  2. 2
    Enter them in the Extension Settings
    Click the Grapper icon → Settings → fill in API URL and API Key → click Save Settings.
  3. 3
    Verify the connection
    Open a LinkedIn profile → click Scan Page → select the contact → click Save to MongoDB. You should see a green "Saved" confirmation.
⚠ For backend admins: To generate a new API key, run npm run create-key in the backend/ folder. The plaintext key is shown once — copy it and send it securely to your user.

  • Name, email, phone, job title, company, location, website, LinkedIn profile URL
  • Source URL and domain where the contact was extracted from
  • Duplicate detection — same contact seen twice increments a "seen count" instead of creating a duplicate
  • Searchable via GET /api/contacts?q=name&domain=example.com

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

⚠ GHL export is proxied through the Grapper backend. You must have the backend running and configured (see the MongoDB tab) before setting up GHL.
  1. 1
    Get your GHL Location ID
    Log into your GHL account → switch to the sub-account you want to use → go to Settings → Business Profile. The Location ID is listed there (a long alphanumeric string).
  2. 2
    Create a Private Integration Token
    In GHL → Settings → Integrations → Private Integrations → click + New Integration. Give it a name (e.g. "Grapper"). Under scopes, enable at minimum:
    • Contacts — Read & Write
    • Custom Fields — Read & Write (for LinkedIn/Job Title fields)
    Click Save, then copy the generated token.
  3. 3
    Enter credentials in the Extension Settings
    Click the Grapper icon → Settings → fill in GHL Location ID and GHL Access Token (PIT) → click Save Settings.

    The token is sent to the backend, encrypted with AES-256, and stored in MongoDB. You will then see a masked version (e.g. ••••••••••••abcd) — the raw token is never sent back to your browser again.
  4. 4
    Send a contact to GHL
    Open a LinkedIn profile → Scan Page → select the contact → click Send to GHL. The contact appears in your GHL sub-account with LinkedIn Profile, Job Title, Location, and Company Website mapped to custom fields.
✓ Auto-save: When you send to GHL, contacts are also automatically saved to MongoDB in the background — you don't need to click "Save to MongoDB" separately.

  • First name, last name, email, phone, company, job title, city
  • LinkedIn Profile URL → custom field linkedin_profile
  • Job Title → custom field job_title
  • Location → custom field location
  • Company website → custom field company_website
  • Tags: LinkedIn, Grapper
  • Source: "Grapper LinkedIn Extension"
  • Upsert — existing contacts with the same email are updated, not duplicated

Chrome only  ·  Questions? Check the README or contact whoever shared this link.