Async lead processing with inbox/outbox pattern and HMAC-signed webhooks
Procesamiento asíncrono de leads con patrón inbox/outbox y webhooks firmados con HMAC
The CRM Relay module enables providers to submit leads (individual or batch), which are processed asynchronously and forwarded to clients via signed webhooks. Clients can update lead status, triggering automatic notifications to providers.
El módulo CRM Relay permite a los proveedores enviar leads (individuales o en lote), que se procesan de forma asíncrona y se reenvían a los clientes mediante webhooks firmados. Los clientes pueden actualizar el estado del lead, activando notificaciones automáticas a los proveedores.
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.
| 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 |
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
}
Update lead state with automatic normalization and transition validation.
Actualiza el estado del lead con normalización automática y validación de transiciones.
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 |
EN_ESPERA ✓ APROBADO, CANCELADO
APROBADO ✓ CONFIRMADO, CANCELADO
CONFIRMADO ✓ EN_TRANSITO, CANCELADO
EN_TRANSITO ✓ EN_BODEGA, CANCELADO
EN_BODEGA ✓ CANCELADO
CANCELADO ✗ (final state / estado final)
{
"success": true,
"message": "Estado actualizado a APROBADO",
"estado_anterior": "EN_ESPERA",
"estado_nuevo": "APROBADO"
}
{
"success": false,
"message": "Transición no permitida de EN_ESPERA a EN_TRANSITO"
}
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.
| 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.
# 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>"
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
[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;
| 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 |