4.0 KiB
4.0 KiB
Webhook de notificación de pagos — Implementación Laravel
Contexto
El sistema soft_usite (Go/Fiber) envía una notificación HTTP POST a una URL configurable
cada vez que se confirma un pago mediante Bold o dLocal.
El proveedor SaaS debe exponer un endpoint en Laravel para recibirla.
1. Ruta
POST /api/pagos/notificacion
- Registrar en
routes/api.php. - Excluir del middleware CSRF (
VerifyCsrfTokenno aplica en rutasapi, pero verificar que no haya middleware adicional que lo requiera).
2. Autenticación
El sistema envía un header de API key configurable (nombre y valor definidos en saas_api_configs).
Por defecto el header es X-API-Key.
Variable .env |
Descripción |
|---|---|
WEBHOOK_SECRET |
Valor secreto que debe coincidir con el configurado en soft_usite |
WEBHOOK_HEADER |
Nombre del header (default: X-API-Key) |
Si el header no coincide → responder 401 Unauthorized.
3. Body JSON recibido
{
"contrato_id": 42,
"referencia": "contrato-42",
"email": "cliente@ejemplo.com",
"monto": 150000.00,
"moneda": "COP",
"fuente": "bold"
}
| Campo | Tipo | Descripción |
|---|---|---|
contrato_id |
integer |
ID del contrato en soft_usite |
referencia |
string |
Formato "contrato-{id}" |
email |
string |
Email del pagador |
monto |
float |
Monto pagado |
moneda |
string |
ISO 4217: COP, USD, etc. |
fuente |
string |
"bold" o "dlocal" |
4. Lógica a implementar
- Validar el body con las reglas mínimas (
contrato_idrequired integer,referenciarequired string, etc.). - Idempotencia: buscar en
webhook_logssi ya existe un registro con la mismareferencia+fuenteprocesado. Si existe → responder200 {"ok": true}sin reprocesar. - Guardar log en tabla
webhook_logscon todos los campos másipyraw_body. - Buscar el contrato local (o el recurso equivalente) por
contrato_id. - Marcar como pagado:
pago_confirmado = true,fecha_pago = now(). - Disparar evento/job extensible:
PagoConfirmadoEventoProcesarPagoJob— envío de correo, activación de cuenta, etc.
5. Respuesta
Siempre responder HTTP 200 con {"ok": true}, incluso ante errores internos.
Los errores deben loguearse con Log::error() sin exponer detalles en la respuesta,
para evitar reintentos innecesarios desde el sistema externo.
6. Tabla webhook_logs
Schema::create('webhook_logs', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('contrato_id');
$table->string('referencia')->index();
$table->string('email')->nullable();
$table->decimal('monto', 12, 2)->nullable();
$table->string('moneda', 10)->nullable();
$table->string('fuente', 20)->nullable(); // bold | dlocal
$table->string('ip', 45)->nullable();
$table->text('raw_body')->nullable();
$table->boolean('procesado')->default(false);
$table->timestamps();
$table->unique(['referencia', 'fuente']); // idempotencia
});
7. Archivos a generar
| Archivo | Descripción |
|---|---|
app/Http/Controllers/Api/WebhookPagoController.php |
Controlador principal |
app/Models/WebhookLog.php |
Modelo Eloquent |
database/migrations/xxxx_create_webhook_logs_table.php |
Migración |
app/Events/PagoConfirmado.php |
Evento (opcional pero recomendado) |
app/Listeners/ProcesarPagoConfirmado.php |
Listener del evento |
routes/api.php |
Registro de la ruta |
.env.example |
Variables WEBHOOK_SECRET y WEBHOOK_HEADER |
8. Variables .env.example
WEBHOOK_SECRET=tu_clave_secreta_aqui
WEBHOOK_HEADER=X-API-Key
9. Notas de seguridad
- No exponer detalles de error en la respuesta JSON.
- El valor de
WEBHOOK_SECRETdebe tener al menos 32 caracteres aleatorios. - Registrar siempre la IP de origen (
$request->ip()) en el log. - No procesar el pago si el log ya existe (idempotencia por
referencia + fuente). - Comparar el API key con
hash_equals()para evitar timing attacks.