Automate GHL Client Onboarding with Snapshots and Custom Objects

Published on March 21,2026

If you run a GoHighLevel agency and you're onboarding new clients regularly, you've probably already felt this. Every new sub-account needs the same pipelines, the same stages, the same structure. You set it up once, then you set it up again, then again. And if you're syncing data from an external system into those sub-accounts, you also need a way to know which client's data goes where.

Here's the good news: after this setup, adding a new client is one API call and you never touch it again. GHL handles the rest. Here are the two APIs that make that possible.

Snapshots deploy your setup automatically

You create a snapshot from inside GHL. Pick a source sub-account, select the assets you want (pipelines, stages, funnels), and GHL packages it. You get a snapshot ID.

Pass that ID when creating a new sub-account:

POST https://services.leadconnectorhq.com/locations/

{
  "name": "Client Sub-Account",
  "companyId": "your_agency_id",
  "phone": "+1410039940",
  "city": "New York",
  "state": "New York",
  "country": "US",
  "timezone": "US/Central",
  "snapshotId": "your_snapshot_id"
}

The new sub-account comes out with everything already configured. One field. Done.

A custom object as the mapping layer

You could store the external ID to GHL location ID mapping in your own database. But then every time an agency adds a client, someone has to touch your code. The agency can't self-serve.

So I stored it inside GHL instead.

GHL lets you define custom objects, tables with your own fields. Think of it as a lookup table that lives inside GHL: one column for your external ID, one for the GHL location ID. The agency manages it directly. No code changes.

In my case, the object had two fields: skyslope_office_id (your external system's ID) and ghl_location_id (the corresponding GHL sub-account). Each row mapped one client to its GHL location. But the pattern works for any external system.

Setting up the custom object (the only manual step)

This is the only part of this whole setup you do by hand, and you only do it once. GHL doesn't expose a great API for creating object schemas yet, so it's UI only:

Once created, click into the object and look at the browser URL. The ID in the URL is your objectId. Copy it, you'll need it for every API call.

Querying the custom object at runtime

Once the mapping lives inside GHL, your sync service needs to read it before every run. That means querying the custom object programmatically and building the ID map in memory.

Here's the part that caught me off guard: this endpoint is a POST. Not a GET. And it's not under contacts or locations in the docs where you'd naturally go looking. It's tucked under the custom objects section.

You can find it here: Custom Objects Records Search

POST https://services.leadconnectorhq.com/objects/{objectId}/records/search

The objectId is the ID of the custom object you created, not a record inside it. Pass your primary agency location ID in the body:

{
  "locationId": "your_primary_location_id",
  "page": 1,
  "pageLimit": 100
}

Headers:

Authorization: Bearer YOUR_ACCESS_TOKEN
Version: 2021-04-15
Content-Type: application/json

Response comes back with a records array and a total. Each record has a properties object containing your custom fields. Paginate by incrementing page until records.length < pageLimit or you've hit total. Build your map from there and every subsequent sync call knows exactly which location to target.

const map = {};
let page = 1;
let fetched = 0;
let done = false;
while (!done) {
  const res = await searchRecords({ page, pageLimit: 100 });
  for (const r of res.records) {
    map[r.properties.skyslope_office_id] = r.properties.ghl_location_id;
  }
  fetched += res.records.length;
  done = fetched >= res.total || res.records.length < 100;
  page++;
}

The full flow

Do the custom object setup once. After that, everything runs itself.

New client onboards. One API call creates their sub-account with snapshotId, pipelines and stages ready immediately. A GHL workflow fires after the sub-account is created and automatically writes a new record into the custom object, mapping the external ID to the new GHL location ID. No manual entry. No code change.

Every sync run after that queries the custom object, builds the map, and routes each deal to the right sub-account automatically.

GHL set itself up. GHL holds the map. GHL keeps it updated. The sync just reads it.

If you'd like this exact workflow set up for your business, or customised to your sales process, book a discovery call with me.



More in this category: GHL Won't Let You Add Multiple Contacts to an Opportunity. Here's the Fix. ยป