Files
soft_usite/WEBHOOK_LARAVEL.md
2026-05-11 21:41:30 -05:00

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 (VerifyCsrfToken no aplica en rutas api, 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

  1. Validar el body con las reglas mínimas (contrato_id required integer, referencia required string, etc.).
  2. Idempotencia: buscar en webhook_logs si ya existe un registro con la misma referencia + fuente procesado. Si existe → responder 200 {"ok": true} sin reprocesar.
  3. Guardar log en tabla webhook_logs con todos los campos más ip y raw_body.
  4. Buscar el contrato local (o el recurso equivalente) por contrato_id.
  5. Marcar como pagado: pago_confirmado = true, fecha_pago = now().
  6. Disparar evento/job extensible: PagoConfirmadoEvent o ProcesarPagoJob — 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_SECRET debe 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.