Connect with the API

Send Google Sheets rows to GuardenerAI.

Rows in a Google Sheet can become leads in GuardenerAI through the public API. This is a connection you build with a short Apps Script, not a native integration: the script runs on a timer, sends every row not yet marked as sent, finds the lead by email, creates or updates it, and writes the lead’s id back to the sheet.

Not a native integration. The script reads your sheet and writes leads into GuardenerAI. Changes made in GuardenerAI do not flow back to the sheet.

What you need

  • A GuardenerAI API key with the Member role and Read and write scope, created by an admin under Settings, API keys.
  • A Google account that can edit the sheet and its Apps Script project (Extensions, Apps Script).
  • A sheet named Leads with these columns in order: First name, Last name, Email, Phone, Company, Notes, GuardenerAI ID, Sent.

Step by step

  1. Store the key in Script Properties

    In the Apps Script editor open Project Settings, then Script Properties, and add a property named GUARDENERAI_KEY with your key as the value. Keep it out of the code itself. Anyone with edit access to the script can read it, so limit who can edit.

  2. Paste the script

    Replace the contents of Code.gs with the script below and change YOUR-ORG to your workspace. For each row with an empty Sent cell it searches for the lead by email, updates the match or creates a new lead, then fills in the id and the time. A row that fails gets the error in its Sent cell, so it is not retried until you clear it. The short pause between rows keeps it well under the API’s rate limit.

    Code.gs
    const BASE = 'https://YOUR-ORG.guardenerai.com/api/leads';
    
    function syncLeads() {
      const key = PropertiesService.getScriptProperties().getProperty('GUARDENERAI_KEY');
      const sheet = SpreadsheetApp.getActive().getSheetByName('Leads');
      const rows = sheet.getDataRange().getValues();
      for (let i = 1; i < rows.length; i++) {
        const [first, last, email, phone, company, notes, , sent] = rows[i];
        if (sent || !email) continue;
        const lead = clean({
          firstName: first, lastName: last,
          email: String(email).trim().toLowerCase(),
          phone: phone, company: company, message: notes,
          sourceMedium: 'google sheet'
        });
        try {
          const found = call('get', BASE + '?filter=email:equals:' + encodeURIComponent(lead.email), key);
          const saved = found.totalElements > 0
            ? call('put', BASE + '/' + found.content[0].id, key, Object.assign({}, found.content[0], lead))
            : call('post', BASE, key, lead);
          sheet.getRange(i + 1, 7, 1, 2).setValues([[saved.id, new Date()]]);
        } catch (e) {
          sheet.getRange(i + 1, 8).setValue('Error: ' + e.message);
        }
        Utilities.sleep(300);
      }
    }
    
    function call(method, url, key, body) {
      const opts = { method: method, headers: { 'X-Api-Key': key }, muteHttpExceptions: true };
      if (body) { opts.contentType = 'application/json'; opts.payload = JSON.stringify(body); }
      const res = UrlFetchApp.fetch(url, opts);
      if (res.getResponseCode() >= 300) throw new Error(res.getResponseCode() + ' ' + res.getContentText());
      return JSON.parse(res.getContentText());
    }
    
    function clean(obj) {
      Object.keys(obj).forEach(function (k) { if (obj[k] === '' || obj[k] == null) delete obj[k]; });
      return obj;
    }
  3. Run it once by hand

    Choose syncLeads and Run. Google asks you to authorise the script to reach external services; approve it. Check that the test rows now show an id, and that the leads appear in GuardenerAI.

  4. Put it on a timer

    Open Triggers, choose Add Trigger, pick syncLeads with a Time-driven event source, set how often it runs, and save. Use a timer rather than an on-edit trigger: rows added by other scripts, forms tools or APIs do not fire edit triggers, and a timer catches every row.

Before you rely on it

What to know about this connection.

With Google Sheets

  • The update copies the lead it found and overwrites only the fields the row fills in, because an update replaces the whole record. Blank cells leave the existing value alone.
  • Email matching is exact and case-sensitive, which is why the script lower-cases emails. Leads already saved with capital letters will not match.
  • Google limits Apps Script: URL Fetch calls are capped at 20,000 a day on a consumer account and 100,000 on Google Workspace, and one run stops after 6 minutes. A long sheet is finished over several runs, since the Sent column picks up where the last one stopped.
  • The script runs as the person who created the trigger. If their access to the sheet is removed, it stops.

With every API connection

  • The key must have the Member role and Read and write scope. A Read only key gets a 403 on every write.
  • There is no upsert. Search for the record by email first, then create it or update the one you found.
  • There are no outbound webhooks. Data flows into GuardenerAI; nothing tells the other tool when a record changes, so anything reading back has to ask on a schedule.
  • Each key allows 10 requests per second. Above that the API answers 429 with Retry-After: 1, so wait a second and retry.
  • An update with PUT replaces the whole record. Send every field you want to keep, not only the one that changed.
  • If one of your duplicate rules blocks it, a create answers 409 duplicate_blocked with the matching records. Update the match instead of forcing a second copy.

Checked against Google Sheets’s own documentation: UrlFetchApp reference, Installable triggers, Properties service, Apps Script quotas. Their plans and features change, so confirm the current details there.

Where it lands

The modules this feeds.

FAQ

Fair questions.

Is this a native Google Sheets integration?
No. GuardenerAI does not ship a Google Sheets connector. This is a script you add to your own sheet, calling the public API with your key.
Can changes in GuardenerAI update my sheet?
Not on their own. There are no outbound webhooks. A script could read leads back from the API on a timer, but the one here only sends rows in.
Should I use this for a one-off import?
For a one-off load, save the sheet as CSV and use the importer in GuardenerAI. The script suits a sheet that keeps getting new rows.