Files
MP-Manager/Documentación de usuario encontrada/GHL-Users-API-Complete.md
T
2026-05-30 14:31:19 -06:00

799 lines
24 KiB
Markdown

# 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"])
```