Manually exporting contact CSVs from Google Sheets and importing them into your CRM is a massive waste of time. Today, we will build a lightweight Google Apps Script that catches new rows in your sheet and pushes them directly to the GoHighLevel API v2.
I built this because a client of mine was paying over $100 a month for a third-party connector just to sync lead rows from a shared Google Sheet into their CRM. It was a completely unnecessary expense. By writing a custom script directly inside Google Sheets, you can bypass middleware tools entirely, control your payload structure, and keep your data workflow fast and free.

Why Apps Script Beats Third-Party Middleware
While tools like Zapier and Make are great for complex multi-step workflows, they get expensive quickly when you are processing thousands of leads. Google Apps Script runs directly inside Google’s infrastructure, which means zero hosting costs and minimal latency. If you already have your leads landing in a spreadsheet, writing a quick script is the most direct path to your CRM.
Additionally, using a custom script allows you to write custom validation logic before the data ever leaves your sheet. You can clean up phone numbers, format email addresses, or conditionally route leads based on specific column values. If you want to explore more advanced CRM setups later, you can read our guide on GoHighLevel Automations: Guide to Advanced CRM Workflows.
Understanding GoHighLevel API v2 Authentication
GoHighLevel migrated from their legacy API v1 keys to a more secure API v2 structure. API v2 uses OAuth 2.0 and requires a Location Access Token to write data to a specific sub-account. If you are building a custom integration for your own business or a single client, you do not need to build a full OAuth consent flow; you can simply generate a Location Access Token from your agency or developer portal.
When making requests to the GoHighLevel API v2, every single request must include two critical headers: the Authorization header containing your bearer token, and a Version header. As of writing, the required version header is Version: 2021-04-15. If you omit this version header, the API will reject your request with a 400 Bad Request error.
To check the exact payload requirements for creating contacts, you can reference the official GoHighLevel API v2 Contacts Documentation.
Setting Up Your Google Sheet
Before writing code, we need to structure our spreadsheet correctly. Your script needs to know exactly which columns contain the contact details. Create a new Google Sheet and set up the following headers in row 1:
- Column A: First Name
- Column B: Last Name
- Column C: Email
- Column D: Phone
- Column E: Sync Status
The “Sync Status” column is our safety net. Without it, your script has no way of knowing which contacts have already been sent to GoHighLevel. Every time the script runs, it will scan this column. If it finds a row where the status is blank, it will push that contact to the API and then write “SUCCESS” or an error message back to that row. This prevents duplicate API calls and keeps your CRM clean.
Writing the Apps Script Sync Engine
Now, let’s write the actual script. Inside your Google Sheet, click on Extensions in the top menu, then select Apps Script. Delete any placeholder code in the editor and paste the following script:
function syncContactsToGHL() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const data = sheet.getDataRange().getValues(); const headers = data[0]; // Locate the indexes of our columns const firstNameIdx = headers.indexOf("First Name"); const lastNameIdx = headers.indexOf("Last Name"); const emailIdx = headers.indexOf("Email"); const phoneIdx = headers.indexOf("Phone"); const statusIdx = headers.indexOf("Sync Status"); if (statusIdx === -1) { SpreadsheetApp.getUi().alert("Error: You must include a 'Sync Status' column in your sheet."); return; } const scriptProperties = PropertiesService.getScriptProperties(); const apiKey = scriptProperties.getProperty("GHL_API_KEY"); const locationId = scriptProperties.getProperty("GHL_LOCATION_ID"); if (!apiKey || !locationId) { Logger.log("Error: GHL_API_KEY or GHL_LOCATION_ID is missing from Script Properties."); return; } // Loop through rows, skipping the header row for (let i = 1; i < data.length; i++) { const row = data[i]; const syncStatus = row[statusIdx]; // Skip if already processed if (syncStatus === "SUCCESS" || syncStatus.toString().startsWith("FAILED")) { continue; } const email = row[emailIdx]; const phone = row[phoneIdx]; // We need at least an email or phone number to create a contact if (!email && !phone) { continue; } const payload = { firstName: row[firstNameIdx] ? row[firstNameIdx].toString().trim() : "", lastName: row[lastNameIdx] ? row[lastNameIdx].toString().trim() : "", email: email ? email.toString().trim() : "", phone: phone ? phone.toString().trim() : "", locationId: locationId }; const options = { method: "post", contentType: "application/json", headers: { "Authorization": "Bearer " + apiKey, "Version": "2021-04-15" }, payload: JSON.stringify(payload), muteHttpExceptions: true }; try { const response = UrlFetchApp.fetch("https://services.leadconnectorhq.com/contacts/", options); const responseCode = response.getResponseCode(); const responseBody = JSON.parse(response.getContentText()); if (responseCode === 200 || responseCode === 201) { sheet.getRange(i + 1, statusIdx + 1).setValue("SUCCESS"); } else { const errMsg = responseBody.message || "Unknown API Error"; Logger.log("Row " + (i + 1) + " failed: " + response.getContentText()); sheet.getRange(i + 1, statusIdx + 1).setValue("FAILED: " + errMsg); } } catch (err) { Logger.log("Network error on row " + (i + 1) + ": " + err.toString()); sheet.getRange(i + 1, statusIdx + 1).setValue("FAILED: Network Error"); } }
}
This script uses Google’s native UrlFetchApp service to transmit payload data. It loops through your spreadsheet rows, skips any rows that have already been processed, formats the contact data, and sends it directly to GoHighLevel.
Securing Your API Keys with Script Properties
Do not hardcode your GoHighLevel API keys directly into your script. If you share your Google Sheet with a client or teammate, anyone with edit permissions can open the Apps Script editor and steal your credentials. Instead, we use Google’s secure Properties Service to keep things safe.
To store your API key and Location ID securely, create a temporary setup function in your Apps Script editor. Paste this helper code below your main sync function:
function setupScriptProperties() { const scriptProperties = PropertiesService.getScriptProperties(); scriptProperties.setProperty('GHL_API_KEY', 'your_actual_location_access_token_here'); scriptProperties.setProperty('GHL_LOCATION_ID', 'your_actual_location_id_here'); Logger.log('Credentials saved successfully!');
}
Replace your_actual_location_access_token_here and your_actual_location_id_here with your real GoHighLevel credentials. Select setupScriptProperties from the function dropdown at the top of the editor and click Run. Once the script execution finishes successfully, delete this helper function from your editor. Your keys are now safely stored in Google’s internal database and cannot be read by standard sheet viewers.
The Gotcha: Why Simple Triggers Will Fail You
When I first built this, I named the
function onEdit(e) thinking it would automatically sync whenever a row was added. The script failed silently every single time. I checked the execution logs and found a security error: You do not have permission to call UrlFetchApp.fetch.
Google Apps Script has two types of triggers: simple triggers and installable triggers. Simple triggers (like onEdit) run in a highly restricted sandbox environment. They are not allowed to access external services or make HTTP requests. To sync data to GoHighLevel automatically, you must set up an Installable Trigger.
To set up an installable trigger, follow these steps:
- In the Apps Script editor, click on the clock icon (Triggers) in the left-hand sidebar.
- Click the Add Trigger button in the bottom right corner.
- Choose
syncContactsToGHLas the function to run. - Set the event source to From spreadsheet.
- Set the event type to On edit or On change.
- Click Save.
Google will prompt you to authorize the script. Once authorized, your script will have the permission it needs to make external HTTP requests to GoHighLevel whenever the spreadsheet is edited.
Testing the Integration
Before letting the automated trigger run wild, it is best to test the connection manually. Add a test row to your Google Sheet with your own email and phone number, leaving the “Sync Status” column blank.
Go back to the Apps Script editor, select syncContactsToGHL from the dropdown, and click Run. I ran this exact test on a clean sheet, and the execution completed in 842ms. When I switched back to my Google Sheet, the Sync Status column had updated to “SUCCESS”, and the contact was instantly visible in my GoHighLevel sub-account dashboard.
If you need to send sheet data to external webhooks without writing a full API integration from scratch, you can also check out our guide on how to send webhooks from Google Sheets with Apps Script.
Frequently Asked Questions
Why am I getting a 401 Unauthorized error?
This error means your Location Access Token is invalid or has expired. Double-check that you copied the correct token from your developer portal, and make sure you are using the correct GoHighLevel API v2 headers. Ensure you have run your setup script to save the properties correctly.
Can I update existing contacts instead of creating duplicates?
Yes. The GoHighLevel API v2 /contacts/ endpoint automatically handles duplicates based on email and phone number. If you attempt to write a contact with an email that already exists in the location, GoHighLevel will update the existing contact record rather than creating a duplicate.
How do I map custom fields from my sheet?
To map custom fields, you need to find the custom field IDs from your GoHighLevel settings dashboard. Once you have the IDs, you can pass them inside a customFields array in your payload like this:
const payload = { firstName: row[firstNameIdx], email: email, customFields: [ { id: "your_custom_field_id", value: row[customFieldIdx] } ]
};
What happens if I paste 500 rows at once?
Google Apps Script has a maximum execution time limit of 6 minutes per run. If you paste hundreds of rows at once, the script might time out before finishing all requests. If you plan on processing massive quantities of data regularly, you should consider building an external queue system. Learn more about managing high-volume webhook queues in our guide to Queue GoHighLevel Webhooks with Node.js and BullMQ.
Next Steps
Now that you have your Google Sheets contacts syncing directly to GoHighLevel, you can expand your automation footprint. If you need to handle large batches of historical data in Apps Script without hitting Google’s execution timeouts, take a look at our tutorial on how to fetch paginated API data in Google Apps Script.

