Simple JWT-authenticated API for lead management
API simple con autenticación JWT para gestión de leads
The CRM API allows providers to create and view leads, while clients can update lead status. All endpoints use JWT authentication.
La API CRM permite a los proveedores crear y ver leads, mientras que los clientes pueden actualizar el estado. Todos los endpoints usan autenticación JWT.
proveedor_lead_id prevents duplicatesproveedor_lead_id único previene duplicadosGet started with the CRM Relay API in 5 simple steps:
Comienza con la API CRM Relay en 5 pasos simples:
POST /api/auth/loginresponse.data.token (nested inside data)Authorization: Bearer <token> to all requestsPOST /api/crm/leads with required fieldsPOST /api/auth/loginresponse.data.token (anidado dentro de data)Authorization: Bearer <token> a todas las peticionesPOST /api/crm/leads con campos requeridoscurl -X POST "http://localhost/paqueteriacz/api/crm/leads" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"lead":{"proveedor_lead_id":"PR-001","nombre":"Juan Pérez","telefono":"+50512345678","fecha_hora":"2025-01-15 10:00:00"}}'
{
"success": true,
"message": "Lead(s) aceptado(s) para procesamiento",
"accepted": 1,
"inbox_id": 123
}
All CRM endpoints require JWT authentication. Use the token from POST /api/auth/login.
Todos los endpoints CRM requieren autenticación JWT. Usa el token de POST /api/auth/login.
Authenticate with your credentials to receive a JWT token:
Autentícate con tus credenciales para recibir un token JWT:
{
"email": "proveedor@example.com",
"password": "your_secure_password"
}
{
"success": true,
"message": "Login exitoso",
"data": {
"id": "123",
"nombre": "Usuario Proveedor",
"email": "proveedor@example.com",
"rol": "Proveedor",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMyIsIm5vbWJyZSI6IlVzdWFyaW8gUHJvdmVlZG9yIiwiZW1haWwiOiJwcm92ZWVkb3JAZXhhbXBsZS5jb20iLCJyb2wiOiJQcm92ZWVkb3IiLCJleHAiOjE3MDQ4MTI0MDB9.signature_here"
}
}
data.token, not at the root level. Extract it as: response.data.token
data.token, no en el nivel raíz. Extráelo como: response.data.token
{
"success": false,
"message": "Credenciales inválidas"
}
# Login and extract token
curl -X POST "http://localhost/paqueteriacz/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"proveedor@example.com","password":"your_password"}'
# Response contains token at data.token
# Use it in subsequent requests:
curl -X GET "http://localhost/paqueteriacz/api/crm/leads" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLC..."
| Role | Permissions |
|---|---|
Proveedor | Create leads, view own leads |
Cliente | Update lead status (ownership required), view assigned leads |
Administrador | Full access + system metrics |
| Rol | Permisos |
|---|---|
Proveedor | Crear leads, ver propios leads |
Cliente | Actualizar estado de lead (requiere propiedad), ver leads asignados |
Administrador | Acceso completo + métricas del sistema |
| Code | Meaning | When |
|---|---|---|
| 200 | OK | Successful query, update, or idempotent retry |
| 202 | Accepted | Lead queued for async processing |
| 400 | Bad Request | Validation error, invalid transition |
| 401 | Unauthorized | Missing/invalid JWT token |
| 403 | Forbidden | Insufficient permissions or ownership |
| 404 | Not Found | Lead ID doesn't exist |
| Código | Significado | Cuándo |
|---|---|---|
| 200 | OK | Consulta exitosa, actualización o reintento idempotente |
| 202 | Aceptado | Lead encolado para procesamiento asíncrono |
| 400 | Solicitud Incorrecta | Error de validación, transición inválida |
| 401 | No Autorizado | Token JWT faltante/inválido |
| 403 | Prohibido | Permisos insuficientes o falta de propiedad |
| 404 | No Encontrado | ID de lead no existe |
Submit individual leads or batches. Returns 202 Accepted immediately.
Envía leads individuales o en lote. Retorna 202 Accepted inmediatamente.
Proveedor, Administrador
{
"lead": {
"proveedor_lead_id": "PR-12345",
"nombre": "Juan Pérez",
"telefono": "+50512345678",
"producto": "Laptop Dell",
"precio": 500.00,
"fecha_hora": "2025-01-15 10:30:00",
"cliente_id": 5
}
}
{
"leads": [
{"proveedor_lead_id":"PR-001","nombre":"Lead 1","telefono":"+50511111111","fecha_hora":"2025-01-15 10:00:00"},
{"proveedor_lead_id":"PR-002","nombre":"Lead 2","telefono":"+50522222222","fecha_hora":"2025-01-15 10:05:00"}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
proveedor_lead_id | string(120) | ✅ Yes | Unique lead ID (per provider) |
fecha_hora | datetime | ✅ Yes | Lead timestamp (YYYY-MM-DD HH:MM:SS) |
nombre | string(255) | No | Prospect name |
telefono | string(30) | No | Phone number |
producto | string(255) | No | Product of interest |
precio | decimal(10,2) | No | Product price |
cliente_id | integer | No | Client ID (auto-forward if has webhook) |
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
proveedor_lead_id | string(120) | ✅ Sí | ID único de lead (por proveedor) |
fecha_hora | datetime | ✅ Sí | Fecha y hora del lead (YYYY-MM-DD HH:MM:SS) |
nombre | string(255) | No | Nombre del prospecto |
telefono | string(30) | No | Número de teléfono |
producto | string(255) | No | Producto de interés |
precio | decimal(10,2) | No | Precio del producto |
cliente_id | integer | No | ID del cliente (auto-reenvío si tiene webhook) |
{
"success": true,
"message": "Lead(s) aceptado(s) para procesamiento",
"accepted": 1,
"inbox_id": 123
}
{
"success": true,
"message": "Lead(s) ya procesado(s) previamente",
"accepted": 1,
"duplicated": true
}
Assign one or multiple leads to a specific client. Providers can only assign their own leads.
Asigna uno o múltiples leads a un cliente específico. Los proveedores solo pueden asignar sus propios leads.
Proveedor (own leads only), Administrador
Proveedor (solo sus propios leads), Administrador
{
"cliente_id": 5,
"lead_ids": [101, 102, 103],
"observaciones": "Asignación manual"
}
| Field | Type | Required | Description |
|---|---|---|---|
cliente_id | integer | ✅ Yes | Target Client ID (User ID with role 'Cliente') |
lead_ids | array|int | ✅ Yes | List of Lead IDs or single ID |
observaciones | string | No | Optional notes |
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cliente_id | integer | ✅ Sí | ID Cliente Destino (ID Usuario con rol 'Cliente') |
lead_ids | array|int | ✅ Sí | Lista de IDs de Leads o ID único |
observaciones | string | No | Notas opcionales |
{
"success": true,
"message": "Operación completada. 3 asignados a 'Cliente Juan'.",
"updated": 3,
"failed": 0,
"total_processed": 3
}
{
"success": true,
"message": "Operación completada. 2 asignados a 'Cliente Juan'. (1 fallos)",
"updated": 2,
"failed": 1,
"total_processed": 3,
"failed_details": [
{"lead_id": 103, "error": "No tienes permiso (No eres el proveedor creador)"}
]
}
Retrieve specific lead metrics for the authenticated provider (or admin view of provider data).
Recupera métricas específicas de leads para el proveedor autenticado (o vista de admin de datos de proveedor).
Proveedor, Administrador
{
"success": true,
"data": {
"total": 150,
"procesados": 120,
"en_espera": 30,
"porcentaje_procesado": 80.0,
"por_estado": [
{"estado": "EN_ESPERA", "cantidad": 30},
{"estado": "APROBADO", "cantidad": 100},
{"estado": "CANCELADO", "cantidad": 20}
]
}
}
Update lead state with automatic normalization. Clients have full flexibility to change to any valid state.
Actualiza el estado del lead con normalización automática. Clientes tienen total flexibilidad para cambiar a cualquier estado válido.
Cliente (owner only), Administrador
Cliente (solo propietario), Administrador
{
"estado": "Aprovado",
"observaciones": "Cliente confirmó recepción"
}
| Canonical State | Accepted Aliases |
|---|---|
EN_ESPERA | ESPERA, PENDING, WAITING |
APROBADO | APROVADO, APPROVED |
CONFIRMADO | CONFIRMAR, CONFIRMED |
EN_TRANSITO | EN TRANSITO, TRANSITO, TRANSIT |
EN_BODEGA | EN BODEGA, BODEGA, WAREHOUSE |
CANCELADO | CANCELAR, CANCELLED, CANCELED |
| Estado Canónico | Alias Aceptados |
|---|---|
EN_ESPERA | ESPERA, PENDING, WAITING |
APROBADO | APROVADO, APPROVED |
CONFIRMADO | CONFIRMAR, CONFIRMED |
EN_TRANSITO | EN TRANSITO, TRANSITO, TRANSIT |
EN_BODEGA | EN BODEGA, BODEGA, WAREHOUSE |
CANCELADO | CANCELAR, CANCELLED, CANCELED |
| State | Description | When to Use |
|---|---|---|
EN_ESPERA | Waiting for approval | Initial state when order is created |
APROBADO | Approved and validated | After reviewing and approving the order |
CONFIRMADO | Confirmed with customer | Customer confirmed they want to proceed |
EN_TRANSITO | Package on the way | Package shipped and being transported |
EN_BODEGA | Package arrived at warehouse | Package received and stored |
CANCELADO | Order cancelled | Order will not proceed for any reason |
| Estado | Descripción | Cuándo Usar |
|---|---|---|
EN_ESPERA | Esperando aprobación | Estado inicial cuando se crea el pedido |
APROBADO | Aprobado y validado | Después de revisar y aprobar el pedido |
CONFIRMADO | Confirmado con cliente | Cliente confirmó que procede con el pedido |
EN_TRANSITO | Paquete en camino | Paquete salió y está siendo transportado |
EN_BODEGA | Paquete llegó a bodega | Paquete recibido y almacenado |
CANCELADO | Pedido cancelado | Pedido no procede por cualquier razón |
{
"success": true,
"message": "Estado actualizado a APROBADO",
"estado_anterior": "EN_ESPERA",
"estado_nuevo": "APROBADO"
}
{
"success": false,
"message": "Estado inválido: INVALID_STATE",
"estados_validos": ["EN_ESPERA", "APROBADO", "CONFIRMADO", "EN_TRANSITO", "EN_BODEGA", "CANCELADO"]
}
Update the status of multiple leads simultaneously. Clients can only update leads they own (cliente_id), while admins can update any leads.
Actualiza el estado de múltiples leads simultáneamente. Los clientes solo pueden actualizar leads que les pertenecen (cliente_id), mientras que los admins pueden actualizar cualquier lead.
Cliente (own leads only), Administrador
Cliente (solo sus propios leads), Administrador
{
"lead_ids": [123, 124, 125],
"estado": "contactado",
"observaciones": "Contactados vía campaña SMS"
}
curl -X POST "http://localhost/paqueteriacz/api/crm/leads/bulk-status" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{ "lead_ids": [123, 124, 125], "estado": "aprobado", "observaciones": "Procesados el 2026-01-02" }'
{
"success": true,
"message": "3 de 3 leads actualizados exitosamente",
"updated": 3,
"failed": 0,
"total": 3,
"estado_nuevo": "APROBADO",
"results": [
{ "lead_id": 123, "success": true, "estado_anterior": "EN_ESPERA", "estado_nuevo": "APROBADO" },
{ "lead_id": 124, "success": true, "estado_anterior": "EN_PROCESO", "estado_nuevo": "APROBADO" },
{ "lead_id": 125, "success": true, "estado_anterior": "EN_ESPERA", "estado_nuevo": "APROBADO" }
]
}
{
"success": true,
"message": "2 de 3 leads actualizados exitosamente",
"updated": 2,
"failed": 1,
"total": 3,
"estado_nuevo": "APROBADO",
"results": [
{ "lead_id": 123, "success": true, "estado_anterior": "EN_ESPERA", "estado_nuevo": "APROBADO" },
{ "lead_id": 124, "success": false, "message": "No tienes permiso para este lead" },
{ "lead_id": 125, "success": true, "estado_anterior": "EN_ESPERA", "estado_nuevo": "APROBADO" }
]
}
{
"success": false,
"message": "Límite máximo de 100 leads por request",
"received": 150
}
{
"success": false,
"message": "0 de 3 leads actualizados exitosamente",
"updated": 0,
"failed": 3,
"total": 3,
"estado_nuevo": "APROBADO",
"results": [
{ "lead_id": 999, "success": false, "message": "Lead no encontrado" },
{ "lead_id": 888, "success": false, "message": "No tienes permiso para este lead" },
{ "lead_id": 777, "success": false, "message": "Lead no encontrado" }
]
}
For processing large datasets (>100 leads). Accepts leads, creates a background job, and returns immediately.
Para procesar grandes conjuntos de datos (>100 leads). Acepta leads, crea un trabajo en segundo plano y retorna inmediatamente.
Cliente (own leads only), Administrador
Cliente (solo sus propios leads), Administrador
{
"lead_ids": [1001, 1002, ..., 5000],
"estado": "CANCELADO",
"observaciones": "Limpieza mensual automática"
}
{
"success": true,
"message": "Proceso de actualización masiva iniciado. Job ID: bulk_6958b3ea2e5ad_1767420906",
"job_id": "bulk_6958b3ea2e5ad_1767420906",
"status_url": "http://localhost/paqueteriacz/api/crm/jobs/bulk_6958b3ea2e5ad_1767420906"
}
Poll this endpoint to track the progress of an async bulk update.
Consulta este endpoint para rastrear el progreso de una actualización masiva asíncrona.
{
"success": true,
"job_id": "bulk_6958b3ea2e5ad_1767420906",
"status": "processing",
"total_leads": 5000,
"processed_leads": 2350,
"progress_percent": 47,
"created_at": "2026-01-03 00:15:06"
}
{
"success": true,
"job_id": "bulk_6958b3ea2e5ad_1767420906",
"status": "completed",
"total_leads": 191,
"processed_leads": 191,
"successful_leads": 3,
"failed_leads": 188,
"estado": "CANCELADO",
"created_at": "2026-01-03 00:15:06",
"started_at": "2026-01-03 00:15:06",
"completed_at": "2026-01-03 00:15:08",
"progress_percent": 100,
"successful_details": [
{"lead_id": 1, "estado_anterior": "EN_ESPERA", "estado_nuevo": "CANCELADO"},
{"lead_id": 5, "estado_anterior": "EN_PROCESO", "estado_nuevo": "CANCELADO"},
{"lead_id": 12, "estado_anterior": "APROBADO", "estado_nuevo": "CANCELADO"}
],
"failed_details": [
{"lead_id": 2, "error": "No encontrado"},
{"lead_id": 3, "error": "Sin permiso"}
]
}
Retrieve leads with filtering and pagination. Automatic role-based scoping applies.
Recupera leads con filtrado y paginación. Se aplica alcance automático basado en roles.
Cliente (own leads only), Administrador
Cliente (solo sus propios leads), Administrador
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 50 | Items per page (max 100) |
estado | string | - | Filter by state |
fecha_desde | date | - | From date (YYYY-MM-DD) |
fecha_hasta | date | - | To date (YYYY-MM-DD) |
| Parámetro | Tipo | Por Defecto | Descripción |
|---|---|---|---|
page | integer | 1 | Número de página |
limit | integer | 50 | Items por página (máx 100) |
estado | string | - | Filtrar por estado |
fecha_desde | date | - | Desde fecha (YYYY-MM-DD) |
fecha_hasta | date | - | Hasta fecha (YYYY-MM-DD) |
curl "http://localhost/paqueteriacz/api/crm/leads?page=1&limit=10&estado=APROBADO" \
-H "Authorization: Bearer <TOKEN>"
Access full lead details or status change history.
Accede a los detalles completos del lead o historial de cambios de estado.
Cliente (own lead only), Administrador
Cliente (solo su propio lead), Administrador
# View detail
curl "http://localhost/paqueteriacz/api/crm/leads/1" \
-H "Authorization: Bearer <TOKEN>"
# View timeline
curl "http://localhost/paqueteriacz/api/crm/leads/1/timeline" \
-H "Authorization: Bearer <TOKEN>"
# Ver detalle
curl "http://localhost/paqueteriacz/api/crm/leads/1" \
-H "Authorization: Bearer <TOKEN>"
# Ver cronología
curl "http://localhost/paqueteriacz/api/crm/leads/1/timeline" \
-H "Authorization: Bearer <TOKEN>"
Manage user notifications. Notifications are generated on key events like lead assignment or status changes.
Gestiona las notificaciones del usuario. Las notificaciones se generan en eventos clave como asignación de leads o cambios de estado.
Proveedor, Cliente, Administrador
{
"success": true,
"data": [
{
"id": 15,
"tipo": "nuevo_lead",
"mensaje": "Nuevo lead asignado: Juan Pérez",
"leido": 0,
"fecha_creacion": "2025-01-15 10:30:00",
"data_adicional": {"lead_id": 123}
}
],
"unread_count": 1
}
Monitor CRM health and performance (admin-only).
Monitorea la salud y rendimiento del CRM (solo administrador).
Administrador only
Solo Administrador
curl "http://localhost/paqueteriacz/api/crm/metrics" \
-H "Authorization: Bearer <ADMIN_TOKEN>"
Receive real-time notifications via cryptographically signed webhooks.
Recibe notificaciones en tiempo real vía webhooks firmados criptográficamente.
$payload = file_get_contents("php://input");
$signature = $_SERVER['HTTP_X_SIGNATURE'];
$secret = 'your_shared_secret';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (hash_equals($expected, $signature)) {
$data = json_decode($payload, true);
// Process webhook / Procesar webhook
} else {
http_response_code(401);
exit;
}
INSERT INTO crm_integrations (user_id, kind, webhook_url, secret, is_active)
VALUES (5, 'cliente', 'https://app.com/webhook', 'secret_123', 1);
Background worker for async processing with 3-second polling interval.
Worker en segundo plano para procesamiento asíncrono con intervalo de sondeo de 3 segundos.
# One-time execution (cron)
php cli/crm_worker.php --once
# Daemon mode (systemd)
php cli/crm_worker.php --loop
# Ejecución única (cron)
php cli/crm_worker.php --once
# Modo daemon (systemd)
php cli/crm_worker.php --loop
Processes asynchronous bulk update jobs from POST /bulk-status-async.
Procesa jobs de actualización masiva asíncrona de POST /bulk-status-async.
# Start worker (runs continuously)
php cli/crm_bulk_worker.php
Removes old jobs and monitors stuck processes. Run daily via Cron/Task Scheduler.
Elimina jobs antiguos y monitorea procesos atascados. Ejecutar diariamente vía Cron/Task Scheduler.
# Run cleanup
php cli/crm_jobs_cleanup.php
[Unit]
Description=CRM Relay Worker
After=mariadb.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/xampp/htdocs/paqueteriacz
ExecStart=/usr/bin/php cli/crm_worker.php --loop
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable crm-worker
sudo systemctl start crm-worker
sudo journalctl -u crm-worker -f
"duplicated":trueps aux | grep crm_workercrm_outbox.last_error columnSELECT COUNT(*) FROM crm_inbox WHERE status='pending'"duplicated":trueps aux | grep crm_workercrm_outbox.last_errorSELECT COUNT(*) FROM crm_inbox WHERE status='pending'-- Pending inbox count
SELECT COUNT(*) FROM crm_inbox WHERE status='pending';
-- Failed webhooks
SELECT id, event_type, attempts, last_error
FROM crm_outbox
WHERE status='failed'
LIMIT 10;
-- Recent leads
SELECT id, proveedor_lead_id, estado_actual, created_at
FROM crm_leads
ORDER BY created_at DESC
LIMIT 20;
-- Conteo de inbox pendientes
SELECT COUNT(*) FROM crm_inbox WHERE status='pending';
-- Webhooks fallidos
SELECT id, event_type, attempts, last_error
FROM crm_outbox
WHERE status='failed'
LIMIT 10;
-- Leads recientes
SELECT id, proveedor_lead_id, estado_actual, created_at
FROM crm_leads
ORDER BY created_at DESC
LIMIT 20;