24 KiB
GoHighLevel Users API — Documentación Completa por Endpoint
Fuentes: GitHub OpenAPI spec · Marketplace Docs · Perplexity Search Base URL:
https://services.leadconnectorhq.comAuth:Authorization: Bearer {TOKEN}|Version: 2021-07-28|User-Agent: Mozilla/5.0
1. POST /users/ — Create User
Crea un nuevo usuario en la agencia y lo asigna a una o más locations.
cURL
curl -X POST https://services.leadconnectorhq.com/users/ \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0" \
-d '{
"companyId": "ve9EPM428h8vShlRW1KT",
"firstName": "John",
"lastName": "Deo",
"email": "john@deo.com",
"password": "*******",
"phone": "+18832327657",
"type": "account",
"role": "admin",
"locationIds": ["C2QujeCh8ZnC7al2InWR"],
"permissions": {
"contactsEnabled": true,
"opportunitiesEnabled": true,
"conversationsEnabled": true
},
"scopes": ["contacts.write", "opportunities.write"]
}'
Python
import http.client
import json
def create_user(token, company_id, first_name, last_name, email, password,
location_ids, role="user", type_="account", phone=None,
permissions=None, scopes=None, language="es"):
"""
POST /users/ — Create a user in GHL.
Args:
token: API token (pit-xxx or OAuth)
company_id: Agency ID
location_ids: List of location IDs to assign user to
permissions: Dict of 39 boolean flags (see §6 of research doc)
scopes: List of scope strings (see §7 of research doc)
Returns:
dict with user data (id, name, email, phone) or error
"""
payload = {
"companyId": company_id,
"firstName": first_name,
"lastName": last_name,
"email": email,
"password": password,
"type": type_,
"role": role,
"locationIds": location_ids,
"platformLanguage": language
}
if phone:
payload["phone"] = phone
if permissions:
payload["permissions"] = permissions
if scopes:
payload["scopes"] = scopes
body = json.dumps(payload).encode()
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("POST", "/users/", body, {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
if res.status == 201:
return data.get("user", data) # Response wraps in 'user' sometimes
else:
return {"error": res.status, "message": data.get("message", str(data))}
# ── Usage ──
user = create_user(
token="pit-xxxxxxxx",
company_id="ve9EPM428h8vShlRW1KT",
first_name="Martin",
last_name="Macedo",
email="abejorrito79@gmail.com",
password="********",
phone="+527775610738",
location_ids=["U0S0QntXgSOz9Fx18Db4"],
role="user",
permissions={
"contactsEnabled": True,
"opportunitiesEnabled": True,
"conversationsEnabled": True,
"assignedDataOnly": True
},
scopes=["contacts.write", "opportunities.write", "conversations.write"]
)
print(user) # {"id": "0IHuJvc2ofPAAA8GzTRi", "name": "Martin Macedo", ...}
Request Body Schema
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
companyId |
string | ✅ | ID de la agencia |
firstName |
string | ✅ | Nombre |
lastName |
string | ✅ | Apellido |
email |
string | ✅ | Email (debe ser único) |
password |
string | ✅ | Contraseña |
type |
string | ✅ | account o agency |
role |
string | ✅ | admin, user, agency_admin |
locationIds |
string[] | ✅ | Array de Location IDs |
phone |
string | Teléfono en E.164 | |
permissions |
object | 39 flags booleanos | |
scopes |
string[] | 189 scopes OAuth | |
scopesAssignedToOnly |
string[] | Scopes asignados | |
profilePhoto |
string | URL de foto | |
twilioPhone |
object | {"locId": "+123"} — semántica replace |
|
platformLanguage |
string | es, en_US, fr_CA, etc. |
Response 201 Created
{
"id": "0IHuJvc2ofPAAA8GzTRi",
"name": "John Deo",
"firstName": "John",
"lastName": "Deo",
"email": "john@deo.com",
"phone": "+1 808-868-8888",
"extension": "",
"permissions": {...},
"scopes": "...",
"roles": {...},
"deleted": false,
"lcPhone": {"locationId": "+1234556677"},
"platformLanguage": "en_US"
}
2. GET /users/{userId} — Get User
Obtiene todos los datos de un usuario por su ID.
cURL
curl -X GET https://services.leadconnectorhq.com/users/{userId} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0"
Python
import http.client
import json
def get_user(token, user_id):
"""GET /users/{userId} — Get user details by ID."""
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("GET", f"/users/{user_id}", "", {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
if res.status == 200:
return data.get("user", data)
return {"error": res.status, "message": data.get("message", str(data))}
# ── Usage ──
user = get_user(token="pit-xxxxxxxx", user_id="0IHuJvc2ofPAAA8GzTRi")
print(user["firstName"], user["email"], user["permissions"])
Response 200
{
"id": "0IHuJvc2ofPAAA8GzTRi",
"name": "John Deo",
"firstName": "John",
"lastName": "Deo",
"email": "john@deo.com",
"phone": "+1 808-868-8888",
"extension": "",
"permissions": {...},
"scopes": "...",
"roles": {...},
"deleted": false,
"lcPhone": {"locationId": "+1234556677"},
"platformLanguage": "en_US"
}
3. PUT /users/{userId} — Update User
Actualiza datos del usuario. Todos los campos son opcionales — solo envía lo que necesites cambiar.
cURL — Cambiar permisos y scopes
curl -X PUT https://services.leadconnectorhq.com/users/{userId} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0" \
-d '{
"permissions": {"contactsEnabled": true, "opportunitiesEnabled": true},
"scopes": ["contacts.write", "opportunities.write"]
}'
cURL — Mover usuario a otra location
curl -X PUT https://services.leadconnectorhq.com/users/{userId} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0" \
-d '{"locationIds": ["LOC_ID_NUEVA"]}'
Python
import http.client
import json
def update_user(token, user_id, **kwargs):
"""
PUT /users/{userId} — Update user fields.
All kwargs are optional. Only send what you want to change.
Examples:
update_user(token, uid, permissions={"contactsEnabled": True})
update_user(token, uid, location_ids=["LOC_A", "LOC_B"])
update_user(token, uid, password="new_password")
update_user(token, uid, scopes=["contacts.write"])
update_user(token, uid, role="admin", type="account")
"""
# Filter out None values
payload = {k: v for k, v in kwargs.items() if v is not None}
if not payload:
return {"error": "No fields to update"}
body = json.dumps(payload).encode()
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("PUT", f"/users/{user_id}", body, {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
if res.status == 200:
return data.get("user", data)
return {"error": res.status, "message": data.get("message", str(data))}
# ── Usage examples ──
# Example 1: Update permissions and scopes
update_user(
token="pit-xxxxxxxx",
user_id="0IHuJvc2ofPAAA8GzTRi",
permissions={
"contactsEnabled": True,
"opportunitiesEnabled": True,
"conversationsEnabled": True,
"assignedDataOnly": True
},
scopes=["contacts.write", "opportunities.write", "conversations.write"]
)
# Example 2: Move user to a different location
update_user(
token="pit-xxxxxxxx",
user_id="0IHuJvc2ofPAAA8GzTRi",
location_ids=["U0S0QntXgSOz9Fx18Db4"] # ZACATEPEC
)
# Example 3: Change role and type
update_user(
token="pit-xxxxxxxx",
user_id="0IHuJvc2ofPAAA8GzTRi",
role="admin",
type="account"
)
# Example 4: Reset password
update_user(
token="pit-xxxxxxxx",
user_id="0IHuJvc2ofPAAA8GzTRi",
password="new_secure_password"
)
Campos actualizables
| Campo | Tipo | Deprecado | Descripción |
|---|---|---|---|
firstName |
string | Nombre | |
lastName |
string | Apellido | |
email |
string | ⚠️ SÍ | Ya no soportado por seguridad |
password |
string | Nueva contraseña | |
phone |
string | Teléfono | |
type |
string | account / agency |
|
role |
string | admin / user / agency_admin |
|
companyId |
string | ID de agencia (requerido para agency-level) | |
locationIds |
string[] | Reasignar sucursales | |
permissions |
object | 39 flags booleanos | |
scopes |
string[] | 189 scopes OAuth | |
scopesAssignedToOnly |
string[] | Scopes asignados | |
profilePhoto |
string | URL de foto | |
twilioPhone |
object | Números inbound — semántica replace | |
platformLanguage |
string | es, en_US, etc. |
4. DELETE /users/{userId} — Delete User
Soft delete asíncrono. El usuario se encola para eliminación y toma efecto en minutos.
cURL
curl -X DELETE https://services.leadconnectorhq.com/users/{userId} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0"
Python
import http.client
import json
def delete_user(token, user_id):
"""DELETE /users/{userId} — Soft delete (async, takes minutes)."""
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("DELETE", f"/users/{user_id}", "", {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
return data # {"succeded": true, "message": "Queued deleting user..."}
# ── Usage ──
result = delete_user(token="pit-xxxxxxxx", user_id="0IHuJvc2ofPAAA8GzTRi")
print(result["message"]) # "Queued deleting user with e-mail john@deo.com..."
Response 200
{
"succeded": true,
"message": "Queued deleting user with e-mail john@deo.com and name John Deo. Will take effect in a few minutes."
}
5. GET /users/search — Search Users
Búsqueda avanzada con filtros por companyId, locationId, nombre, email, rol, tipo, IDs específicos, y ordenamiento.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
Version |
string | ✅ | API Version header |
companyId |
string | ✅ | ID de la agencia |
query |
string | Búsqueda por nombre, email o teléfono | |
locationId |
string | Filtrar por location | |
type |
string | agency, account |
|
role |
string | admin, user, agency_admin |
|
ids |
string | IDs separados por coma | |
skip |
string | Paginación offset (default: 0) | |
limit |
string | Page size (default: 25) | |
sort |
string | Campo de orden: dateAdded |
|
sortDirection |
string | asc o desc |
cURL
curl -X GET "https://services.leadconnectorhq.com/users/search?companyId=5DP41231LkQsiKESj6rh&locationId=C2QujeCh8ZnC7al2InWR&query=John&limit=10&sort=dateAdded&sortDirection=desc" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0"
Python
import http.client
import json
import urllib.parse
def search_users(token, company_id, query=None, location_id=None,
user_type=None, role=None, ids=None, skip=0, limit=25,
sort="dateAdded", sort_direction="desc"):
"""GET /users/search — Advanced user search."""
params = {
"companyId": company_id,
"skip": str(skip),
"limit": str(limit),
"sort": sort,
"sortDirection": sort_direction
}
if query:
params["query"] = query
if location_id:
params["locationId"] = location_id
if user_type:
params["type"] = user_type
if role:
params["role"] = role
if ids:
params["ids"] = ids
qs = urllib.parse.urlencode(params)
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("GET", f"/users/search?{qs}", "", {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
if res.status == 200:
return data # {"users": [...], "count": N}
return {"error": res.status, "message": data.get("message", str(data))}
# ── Usage examples ──
# Find all users in a location
users = search_users(token="pit-xxxxxxxx", company_id="ve9EPM428h8vShlRW1KT",
location_id="U0S0QntXgSOz9Fx18Db4")
print(f"Found {users['count']} users in ZACATEPEC")
# Search by name
users = search_users(token="pit-xxxxxxxx", company_id="ve9EPM428h8vShlRW1KT",
query="Martin Macedo")
for u in users.get("users", []):
print(f" {u['id']}: {u['name']} ({u['email']})")
# Filter by role
admins = search_users(token="pit-xxxxxxxx", company_id="ve9EPM428h8vShlRW1KT",
role="admin", limit=50)
Response 200
{
"users": [
{
"id": "0IHuJvc2ofPAAA8GzTRi",
"name": "John Deo",
"firstName": "John",
"lastName": "Deo",
"email": "john@deo.com",
"phone": "+1 808-868-8888",
"extension": "",
"permissions": {...},
"scopes": "...",
"roles": {...},
"deleted": false,
"lcPhone": {"locationId": "+1234556677"},
"platformLanguage": "en_US"
}
],
"count": 1
}
6. POST /users/search/filter-by-email — Filter by Email
Filtra usuarios por array de emails. Ideal para verificar duplicados antes de crear.
cURL
curl -X POST https://services.leadconnectorhq.com/users/search/filter-by-email \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Version: 2021-07-28" \
-H "User-Agent: Mozilla/5.0" \
-d '{
"companyId": "ve9EPM428h8vShlRW1KT",
"emails": ["john@deo.com", "jane@deo.com"],
"includeDeleted": false
}'
Python
import http.client
import json
def filter_users_by_email(token, company_id, emails, include_deleted=False):
"""
POST /users/search/filter-by-email — Check if emails already exist.
Args:
emails: List of email strings to check
include_deleted: Include soft-deleted users
Returns:
List of users matching the emails
"""
body = json.dumps({
"companyId": company_id,
"emails": emails,
"includeDeleted": include_deleted
}).encode()
conn = http.client.HTTPSConnection("services.leadconnectorhq.com", timeout=30)
conn.request("POST", "/users/search/filter-by-email", body, {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
})
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
if res.status == 200:
return data # Array or {"users": [...]}
return {"error": res.status, "message": data.get("message", str(data))}
# ── Usage: Check for duplicate before creating ──
def email_exists(token, company_id, email):
"""Returns True if a user with this email already exists."""
result = filter_users_by_email(token, company_id, [email])
users = result.get("users", result if isinstance(result, list) else [])
return len(users) > 0
existing = email_exists(token="pit-xxxxxxxx", company_id="ve9EPM428h8vShlRW1KT",
email="abejorrito79@gmail.com")
if existing:
print("⚠️ Email already taken — cannot create user")
else:
print("✅ Email available — safe to create")
Response 200
Devuelve array de usuarios que coinciden con los emails proporcionados.
7. GET /users/ — Get User by Location ⚠️ DEPRECADO
No usar. Reemplazado por GET /users/search con query param locationId.
# ❌ Deprecado
GET /users/?locationId=LOC_ID
# ✅ Correcto
GET /users/search?locationId=LOC_ID&companyId=COMPANY_ID
8. Response Codes
| Código | Significado | Schema |
|---|---|---|
200 |
OK (GET, PUT, DELETE) | UserSuccessfulResponseDto / DeleteUserSuccessfulResponseDto |
201 |
Created (POST) | UserSuccessfulResponseDto |
400 |
Bad Request | BadRequestDTO |
401 |
Unauthorized — token sin scope | UnauthorizedDTO |
422 |
Unprocessable — email duplicado, locationIds inválido | UnprocessableDTO |
9. Scope Requirements
| Operación | Scope |
|---|---|
| GET (search, get by id, filter by email) | users.readonly |
| POST (create) | users.write |
| PUT (update) | users.write |
| DELETE | users.write |
10. Notas importantes
- Email no se puede actualizar — el campo
emailen PUT está deprecado por seguridad - Delete es asíncrono — la respuesta confirma encolado, no eliminación inmediata
- TwilioPhone tiene semántica replace — si lo envías, reemplaza todo el mapa; no hace merge
- Scopes vacíos deshabilitan todo —
"scopes": []quita todos los permisos - companyId es obligatorio en search y filter-by-email — se obtiene del
GET /locations/{id} - locationIds acepta array — un usuario puede pertenecer a múltiples sucursales simultáneamente
- Versión 2021-07-28 — todos los endpoints usan esta versión; algunos aceptan 2023-02-21
11. Python Helper — Clase completa
import http.client
import json
import urllib.parse
class GHLUsersAPI:
"""Complete wrapper for GoHighLevel Users API."""
BASE = "services.leadconnectorhq.com"
def __init__(self, token):
self.token = token
self.headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"User-Agent": "Mozilla/5.0"
}
def _request(self, method, path, body=None):
"""Generic HTTP request helper."""
conn = http.client.HTTPSConnection(self.BASE, timeout=30)
conn.request(method, path, body or "", self.headers)
res = conn.getresponse()
data = json.loads(res.read().decode())
conn.close()
return res.status, data
def create(self, company_id, first_name, last_name, email, password,
location_ids, role="user", type_="account", **kwargs):
"""POST /users/ — Create user."""
payload = {
"companyId": company_id, "firstName": first_name,
"lastName": last_name, "email": email, "password": password,
"type": type_, "role": role, "locationIds": location_ids,
"platformLanguage": kwargs.get("language", "es")
}
for k in ("phone", "permissions", "scopes", "profilePhoto", "twilioPhone"):
if k in kwargs and kwargs[k] is not None:
payload[k] = kwargs[k]
status, data = self._request("POST", "/users/",
json.dumps(payload).encode())
if status == 201:
return data.get("user", data)
raise Exception(f"Create failed: {status} {data.get('message', data)}")
def get(self, user_id):
"""GET /users/{userId} — Get user."""
status, data = self._request("GET", f"/users/{user_id}")
if status == 200:
return data.get("user", data)
raise Exception(f"Get failed: {status}")
def update(self, user_id, **kwargs):
"""PUT /users/{userId} — Update user fields."""
payload = {k: v for k, v in kwargs.items() if v is not None}
if not payload:
raise Exception("No fields to update")
status, data = self._request("PUT", f"/users/{user_id}",
json.dumps(payload).encode())
if status == 200:
return data.get("user", data)
raise Exception(f"Update failed: {status} {data.get('message', data)}")
def delete(self, user_id):
"""DELETE /users/{userId} — Soft delete."""
status, data = self._request("DELETE", f"/users/{user_id}")
if status == 200:
return data
raise Exception(f"Delete failed: {status}")
def search(self, company_id, **kwargs):
"""GET /users/search — Search users."""
params = {"companyId": company_id, "skip": "0", "limit": "25"}
for k in ("query", "locationId", "type", "role", "ids", "sort",
"sortDirection"):
if k in kwargs and kwargs[k] is not None:
params[k] = str(kwargs[k])
qs = urllib.parse.urlencode(params)
status, data = self._request("GET", f"/users/search?{qs}")
if status == 200:
return data
raise Exception(f"Search failed: {status}")
def filter_by_email(self, company_id, emails, include_deleted=False):
"""POST /users/search/filter-by-email — Filter by email list."""
payload = {"companyId": company_id, "emails": emails,
"includeDeleted": include_deleted}
status, data = self._request("POST", "/users/search/filter-by-email",
json.dumps(payload).encode())
if status == 200:
return data
raise Exception(f"Filter failed: {status}")
# ── Complete usage example ──
api = GHLUsersAPI(token="pit-xxxxxxxx")
# 1. Check if email exists
try:
existing = api.filter_by_email("ve9EPM428h8vShlRW1KT", ["test@example.com"])
print(f"Email check: {len(existing.get('users', []))} found")
except:
print("Email available")
# 2. Create user
user = api.create(
company_id="ve9EPM428h8vShlRW1KT",
first_name="Martin", last_name="Macedo",
email="abejorrito79@gmail.com", password="********",
phone="+527775610738",
location_ids=["U0S0QntXgSOz9Fx18Db4"],
role="user",
permissions={"contactsEnabled": True, "opportunitiesEnabled": True},
scopes=["contacts.write", "opportunities.write"]
)
print(f"Created: {user['id']}")
# 3. Update permissions
api.update(user["id"],
permissions={"contactsEnabled": True, "opportunitiesEnabled": True,
"conversationsEnabled": True, "assignedDataOnly": True},
scopes=["contacts.write", "opportunities.write", "conversations.write"])
# 4. Search users in location
result = api.search("ve9EPM428h8vShlRW1KT",
locationId="U0S0QntXgSOz9Fx18Db4")
print(f"Users in ZACATEPEC: {result['count']}")
# 5. Delete user (if needed)
# api.delete(user["id"])