# GoHighLevel Users API — Documentación Completa por Endpoint > **Fuentes:** [GitHub OpenAPI spec](https://github.com/GoHighLevel/highlevel-api-docs/blob/main/apps/users.json) · [Marketplace Docs](https://marketplace.gohighlevel.com/docs/ghl/users/) · 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 ```bash 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 ```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` ```json { "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 ```bash 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 ```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` ```json { "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 ```bash 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 ```bash 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 ```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 ```bash 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 ```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` ```json { "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 ```bash 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 ```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` ```json { "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 ```bash 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 ```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`. ```bash # ❌ 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 ```python 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"]) ```