GoHighLevel + Google Workspace Integration Without Make or Zapier

You can connect GoHighLevel to Google Sheets, Drive, and Gmail without Make or Zapier — using a Google Apps Script web app as a free webhook receiver that GHL posts data to directly.

GoHighLevel handles CRM, pipelines, email sequences, and funnels well. But most service businesses running GHL haven't left Google Workspace — they still track deals in Sheets, share documents in Drive, and run team communication through Gmail. GHL becomes the front-end; Workspace stays the back-office.

The obvious way to connect them is Make or Zapier. Both work. But you're paying $16–$20/month for middleware that does one job: receive a GHL webhook and write a row to a Sheet. Google Apps Script does the same thing for free, runs on your Google account, and gives you complete control over the data shape.

Here's how to set it up properly — including the parts most tutorials skip over.


How the Architecture Works

GHL has a built-in Custom Webhook workflow action. You configure a GHL workflow to fire when something happens (contact created, stage changed, form submitted, etc.), and it sends a POST request with a JSON payload to any URL you give it.

Google Apps Script can be deployed as a web app — a public HTTPS URL that accepts POST requests. Your Script receives the GHL payload, parses it, and writes to any Google service: Sheets, Drive, Gmail, Calendar, Docs.

No third-party platform sitting in the middle. GHL talks directly to your script.


Step 1: Write the Apps Script Receiver

Open Google Sheets → Extensions → Apps Script. Replace the default code:

function doPost(e) {
  try {
    var data = JSON.parse(e.postData.contents);
    var sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .getSheetByName("GHL Leads");

    sheet.appendRow([
      new Date(),
      data.contact_id   || "",
      data.first_name   || "",
      data.last_name    || "",
      data.email        || "",
      data.phone        || "",
      data.pipeline_stage_name || "",
      data.source       || "",
      data.tags         ? data.tags.join(", ") : ""
    ]);

    return ContentService
      .createTextOutput(JSON.stringify({ status: "ok" }))
      .setMimeType(ContentService.MimeType.JSON);

  } catch (err) {
    return ContentService
      .createTextOutput(JSON.stringify({ status: "error", message: err.toString() }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

A few things happening here that matter:

  • e.postData.contents not e.parameter — GHL sends JSON in the request body, not as URL query parameters. Using e.parameter will get you nothing.
  • Fallback to empty string — GHL's webhook payload varies by trigger type. A contact creation event includes different fields than a pipeline stage change. The || "" fallbacks prevent the script from breaking on missing fields.
  • Always return a response — GHL expects a 200 OK. If your function throws without returning, GHL marks the webhook as failed.

Step 2: Deploy as a Web App (This Step Is Where Most People Get It Wrong)

In Apps Script: Deploy → New Deployment → Web App.

Critical settings:

  • Execute as: Me — the script runs under your Google account and has access to your Sheets
  • Who has access: Anyone — GHL's servers need to reach the URL without authentication

Copy the web app URL. It looks like: https://script.google.com/macros/s/ABC.../exec

Every time you redeploy after code changes, you get a new URL. If you've already configured GHL with the old URL, you need to update it. This is the single most common cause of silent webhook failures — the script was updated, the URL changed, GHL is still sending to the old one.

To avoid this: keep a named bookmark of the current URL, and whenever you redeploy, update the GHL workflow immediately.


Step 3: Configure the GHL Workflow

In GoHighLevel: Automation → Workflows → create or open a workflow.

  1. Set your trigger (e.g. "Contact Created", "Pipeline Stage Changed", "Form Submitted")
  2. Add an action → Custom Webhook
  3. Method: POST
  4. URL: your Apps Script web app URL
  5. Content Type: application/json
  6. Body: leave on "Contact Fields" or build a custom JSON payload with the fields you need

Test with GHL's built-in "Test Workflow" feature. Check your Sheet for a new row within a few seconds.


What the GHL Webhook Payload Actually Looks Like

GHL sends different payloads depending on the trigger. For a contact-based trigger, the typical payload structure is:

{
  "contact_id": "abc123",
  "first_name": "Jane",
  "last_name": "Smith",
  "email": "jane@example.com",
  "phone": "+1234567890",
  "source": "Facebook Ad",
  "pipeline_stage_name": "New Lead",
  "tags": ["inbound", "facebook"],
  "custom_fields": {
    "company": "Acme Corp",
    "budget": "5000"
  }
}

Custom fields are nested under custom_fields. If you need them in your Sheet:

data.custom_fields ? (data.custom_fields.company || "") : ""

Log the raw payload first if you're unsure of the structure. Add a temporary line to your script:

Logger.log(JSON.stringify(data));

Then check Apps Script → Executions to see what GHL actually sent.


The Gotchas That Aren't in Any Tutorial

1. Apps Script execution quotas

Free Google Workspace accounts get 90 minutes of total script execution time per day. Paid Workspace accounts get 6 hours. Each GHL webhook hit takes roughly 1–3 seconds to run. At 100 webhook events/day you're using 5 minutes — well under the limit. At 2,000 events/day, you're at ~100 minutes. Know your volume before assuming this is free at any scale.

2. GHL doesn't guarantee delivery order

If two webhook events fire within milliseconds of each other (which happens on bulk imports), GHL sends them both immediately. Apps Script is single-threaded per execution, so concurrent writes to the same Sheet can result in rows being interleaved or one write failing. For low-volume use (<500 events/day) this almost never happens. For high-volume, add a Utilities.sleep(200) or use a queue via PropertiesService.

3. GHL retries on non-200 responses — but not always

If your script returns an error, GHL may retry. But if the script times out and returns nothing, GHL marks it as delivered. Build explicit error handling (the try/catch above) so failures are visible in GHL's webhook log, not silently lost.

4. The web app URL is not secret

Anyone with the URL can POST to your script. For internal pipelines this is usually fine — the URL is a long random string and practically unguessable. If you need real security, add a shared secret: send a custom header from GHL and validate it in the script before processing the payload.


What Else You Can Do With This Pattern

The same webhook → Apps Script pattern extends to anything in Google Workspace:

  • Drive: Create a folder per new GHL contact, name it after the contact, share it with the relevant team member
  • Docs: Generate a scoped proposal document from a template when a deal reaches a specific pipeline stage
  • Gmail: Send internal team notifications on high-value leads that bypass GHL's email system (useful when you want the email to come from a personal address, not GHL's sending domain)
  • Calendar: Log booked appointments from GHL into a team calendar for visibility

The direction also runs the other way. A Google Form submission, a row added to a Sheet, or a Calendar event can trigger a GHL workflow via GHL's inbound webhook or API. That's a separate setup but the same principle — Apps Script fires a POST to GHL's API endpoint when the Workspace event occurs.


When Make or Zapier Is Still the Right Call

The Apps Script approach isn't always better. Use Make or Zapier when:

  • You need to connect GHL to something outside Google Workspace (Airtable, Notion, Slack, etc.) where Apps Script has no native service
  • You need visual mapping of fields and don't want to write any code
  • You have non-technical team members who need to modify the integration without developer help
  • The volume is high enough to exceed Apps Script quotas and you don't want to manage quota issues

For a business fully in Google Workspace that wants GHL data flowing into Sheets — Apps Script is the leaner path. For anything that touches a third platform, the middleware tools earn their cost.

If you're also evaluating whether GHL is the right platform at all, see our breakdown of GoHighLevel vs. a custom build — including the cost math for businesses already in Google Workspace.


Need GHL and Google Workspace talking to each other? We set this up regularly — from simple lead logging to full bidirectional sync. Tell us what you're trying to connect.

Talk to us

Get the Automation Tool Cheat Sheet

One-page reference: which tool to use, production failure modes, and break-even timelines — from 120+ real projects.

No spam. Unsubscribe any time.