Files
2026-05-30 14:31:19 -06:00

24 KiB

GoHighLevel Users API — Documentación Completa por Endpoint

Fuentes: GitHub OpenAPI spec · Marketplace Docs · Perplexity Search Base URL: https://services.leadconnectorhq.com Auth: 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 ⚠️ 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

  1. Email no se puede actualizar — el campo email en PUT está deprecado por seguridad
  2. Delete es asíncrono — la respuesta confirma encolado, no eliminación inmediata
  3. TwilioPhone tiene semántica replace — si lo envías, reemplaza todo el mapa; no hace merge
  4. Scopes vacíos deshabilitan todo"scopes": [] quita todos los permisos
  5. companyId es obligatorio en search y filter-by-email — se obtiene del GET /locations/{id}
  6. locationIds acepta array — un usuario puede pertenecer a múltiples sucursales simultáneamente
  7. 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"])