← Documentation

Partner API

Connect GoHighLevel

When a contact reaches a stage in GoHighLevel — a form submitted, a tag applied, a payment taken — have that workflow create the client in LaunchSite OS. No Zap in between, no middleware, and nothing for you to host.

Everything here works today. GoHighLevel renames things between plans and releases, so the menu labels below may differ slightly from what you see; the shape of the step will not.

Before you start

  • A Partner API key from Settings → Private Integrations. If you have not minted one, do the Getting started guide first — it takes about two minutes and confirms the key works before you build anything on top of it.
  • A GoHighLevel plan whose workflows include the Webhook (sometimes Custom Webhook) action. This is the step that lets a workflow make an outbound HTTP request.

1. Build the workflow

  1. In GoHighLevel, open Automation → Workflows and create one, or edit an existing one.
  2. Pick your trigger — Form Submitted, Tag Added, Order Submitted, or whatever marks “this person is now a client” in your practice. This choice is yours entirely; we react to whatever you decide.
  3. Add an action and choose Webhook.

2. Configure the request

FieldValue
MethodPOST
URLhttps://launchsite-os.com/api/v1/clients

Headers

GoHighLevel’s header editor is a key/value table. Add these three:

X-Api-Key         lsk_live_your_key_here
Content-Type      application/json
Idempotency-Key   {{contact.id}}

X-Api-Key rather than Authorization purely because it is the less fiddly of the two in this editor — both work identically. If your build of GoHighLevel offers an API Key authentication mode, that sets the same header for you.

Body

Choose JSONas the body type and map GoHighLevel’s contact fields into ours using its {{ }} merge syntax:

{
  "full_name": "{{contact.name}}",
  "email": "{{contact.email}}",
  "phone": "{{contact.phone}}",
  "goals": "{{contact.custom_field_goals}}"
}

Only email is genuinely required. Drop any line whose GoHighLevel field you do not populate — sending an empty string is not the same as omitting the field, and an empty full_name gives you a client with no name.

3. The part that actually matters

Idempotency-Key: {{contact.id}} is the single most important line on this page.

Creating a client here makes a real account and emails a real person their credentials. GoHighLevel retries a webhook step that times out or returns a 5xx — and it cannot know whether the request actually landed before the connection dropped. Without an idempotency key, one slow response gives your client two accounts and two credentials emails.

The GoHighLevel contact id is the right value because it is stable. It survives someone correcting a typo in the email address, which an email-derived key would not. If you omit the header entirely we derive one from the email — better than nothing, but it breaks in exactly the case you would most want it to hold.

On a replay you get the original response back with an Idempotent-Replay: true header, on whatever status the first attempt produced.

4. Getting a client id back out

Every other endpoint — sign-in links, compliance, profile updates — needs a client_id. There are two ways to have one, and you want both.

Keep the id when you create the client

The 201 from POST /v1/clients contains it. Add an Update Contact action after your webhook step and write {{webhook_response.client_id}} into a custom field on the GoHighLevel contact — launchsite_client_id is a reasonable name. Every later workflow reads it from there.

Look it up by email when you do not

Storing the id only helps for clients your integration created. Anyone who signed up before you wired this up, or who the coach added by hand in the app, has an empty custom field. Email is the fallback that always works:

GET https://launchsite-os.com/api/v1/clients?email={{contact.email}}
X-Api-Key: lsk_live_your_key_here
{ "count": 1, "clients": [ { "client_id": "5f1c…", "full_name": "Jane Smith", … } ] }

count: 0 means we do not have them, so create them. count: 1 means use clients[0].client_id and do not create a duplicate.

Use this parameter rather than filtering a list yourself. The match runs inside the query, so it is exact and unaffected by limit. If you instead pulled a page of clients and searched it in your workflow, you would get “not found” for anyone sitting just past the end of that page — and then create a second account for a client you already had.

The shape most integrations end up with

1.  GET  /v1/clients?email={{contact.email}}
      count 0  →  POST /v1/clients          (creates; returns client_id)
      count 1  →  use clients[0].client_id

2.  POST /v1/clients/{client_id}/login-link
      →  send login_url in your own branded SMS

Pair that with "send_welcome": false on the create call and the coach owns the whole first touch: no credentials email from a domain the client does not recognise, no password to type, just a branded message with a link that opens their plan. That is what the sign-in link endpoint exists for.

A sign-in link is a bearer credential — whoever reads the URL is that client until it is used. It is single-use and short-lived, never stored, and capped at 20 per minute. Send it to the client, not to a shared inbox.

5. Test it before you turn it on

  1. Use GoHighLevel’s Test Workflowagainst a contact you own — your own email address, not a real client’s. This creates a genuine account and sends a genuine email.
  2. A 201 means it worked. Check the client appears in LaunchSite OS.
  3. Delete the test client afterwards so your roster stays honest.

Reading the failures

StatusWhat it means
400A field was rejected — the response names which one and why. Usually an unmapped merge field that resolved to an empty string, or a value over its length limit. We reject rather than truncate, so nothing is silently stored half-written.
401Key wrong, revoked, or pasted with a trailing space.
403Real key, wrong scope. Mint one with Partner API selected.
409Either that account already exists, or the same idempotency key is in flight right now. The second case sends Retry-After: 5.
429Over 300 requests a minute for this key. Honour Retry-After. The limit is per key rather than per IP, so a shared GoHighLevel egress address is not counted against you.

Going the other way

This guide covers GoHighLevel calling us. For the reverse — LaunchSite OS posting to a GoHighLevel Inbound Webhook trigger when a check-in lands or compliance drops — see Receive events. Register the URL GoHighLevel gives you under Settings → Private Integrations → Webhook Endpoints.

Choose “Include contact details” for a GoHighLevel endpoint. This is the one setting that will quietly break the integration if you get it wrong. GoHighLevel resolves an incoming webhook to its own contact record by email or phone. An endpoint set to Identifiers onlysends our client id and nothing else, so the workflow matches no contact and runs against nobody — and GoHighLevel reports no error, because from its side nothing went wrong. You would be looking at a workflow that fires and does nothing.

Identifiers only is the tighter mode and worth using where your receiver can look the client up over the Partner API with a key, or where you have separately stored our client id on the GoHighLevel contact as a custom field. Note that a custom field only helps for contacts created after you set it up; everyone already in both systems has it empty.