Descargo de responsabilidad: Este artículo es material educativo de ingeniería de software B2B. Las arquitecturas y fragmentos de código presentados no constituyen asesoría financiera ni garantizan cumplimiento normativo ante reguladores locales. Cada implementación de cobros recurrentes debe ser auditada según la jurisdicción aplicable y los términos de servicio del proveedor de pagos. El uso de este contenido es bajo exclusiva responsabilidad del operador.
1. Fundamentos de Billing Recurrente en Entornos B2B
Los sistemas de cobro recurrente (subscription billing) representan la columna vertebral financiera de plataformas SaaS, marketplaces B2B y servicios digitales en América Latina. A diferencia de las transacciones únicas, el billing recurrente introduce complejidades técnicas que escalan exponencialmente: gestión de estados de suscripción, lógica de prorrateo, reintentos inteligentes ante fallos de pago y reconciliación contable asíncrona.
El error más costoso en fintech LATAM es implementar esta lógica dentro de un monolito WordPress o Laravel acoplado. Cuando el volumen supera los 10,000 suscriptores activos, los cuellos de botella en procesamiento de webhooks, timeouts en APIs externas y bloqueos de base de datos generan pérdida silenciosa de ingresos por facturación fallida no detectada.
La solución arquitectónica probada en producción es la separación del dominio de billing en un microservicio especializado, desacoplado del core de negocio mediante colas de mensajes y comunicación asíncrona. Este patrón permite escalar horizontalmente solo el componente financiero sin afectar el resto de la plataforma, manteniendo compatibilidad nativa con pasarelas como Stripe Billing y PayPal Subscriptions.
2. Patrón de Idempotencia: Prevención de Cobros Duplicados
En entornos distribuidos, las redes fallan. Un webhook de confirmación de pago puede entregarse múltiples veces, o un timeout puede hacer que el cliente reintente la misma operación. Sin protección de idempotencia, esto resulta en cobros duplicados, la causa #1 de chargebacks y pérdida de confianza en plataformas B2B.
El patrón de idempotencia exige que cada operación financiera tenga un identificador único generado por el cliente (Idempotency-Key). El servidor debe verificar si ya procesó esa clave antes de ejecutar cualquier lógica de cobro. Si existe, retorna la respuesta cacheada sin tocar la base de datos ni llamar a la pasarela externa.
| Escenario | Sin Idempotencia | Con Idempotencia |
|---|---|---|
| Webhook duplicado | Cobra 2 veces, genera refund manual | Detecta clave repetida, retorna OK sin procesar |
| Timeout + Retry | Ejecuta transacción nueva, doble cargo | Recupera resultado previo de caché/BD |
| Fallo red post-commit | Cliente reintenta, estado inconsistente | Misma clave = mismo resultado garantizado |
| Costo operativo | Alto (soporte + refunds + auditoría) | Cero intervención manual |
3. Implementación en PHP 8.4: Servicio de Suscripciones
Implementación de clase core para gestión de suscripciones B2B con tipado estricto PHP 8.4. Incluye protección de idempotencia, manejo seguro de webhooks y abstracción multi-pasarela.
<?php
declare(strict_types=1);
namespace App\Billing\Service;
use InvalidArgumentException;
use JsonException;
use RuntimeException;
class SubscriptionService
{
private string $gatewayUrl;
private string $apiKey;
private int $timeout;
public function __construct(string $gatewayUrl, string $apiKey, int $timeout = 30)
{
if (filter_var($gatewayUrl, FILTER_VALIDATE_URL) === false) {
throw new InvalidArgumentException("URL de gateway de billing inválida.");
}
$this->gatewayUrl = $gatewayUrl;
$this->apiKey = $apiKey;
$this->timeout = $timeout;
}
/**
* Crea suscripción con protección de idempotencia nativa
*/
public function createSubscription(array $payload): array
{
if (!isset($payload['customer_id'], $payload['plan_id'], $payload['idempotency_key'])) {
throw new InvalidArgumentException("Payload incompleto: requiere customer_id, plan_id e idempotency_key.");
}
$idempotencyKey = (string) $payload['idempotency_key'];
// Validar formato de clave de idempotencia (UUID v4 recomendado)
if (!preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $idempotencyKey)) {
throw new InvalidArgumentException("Idempotency-Key debe ser UUID v4 válido.");
}
$cleanedPayload = [
'customer_id' => (string) $payload['customer_id'],
'plan_id' => (string) $payload['plan_id'],
'currency' => $payload['currency'] ?? 'USD',
'metadata' => $payload['metadata'] ?? []
];
$ch = curl_init($this->gatewayUrl . '/v1/subscriptions');
if ($ch === false) {
throw new RuntimeException("Error inicializando cURL para billing service.");
}
try {
$jsonData = json_encode($cleanedPayload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
} catch (JsonException $e) {
curl_close($ch);
throw new RuntimeException("Serialización JSON fallida: " . $e->getMessage());
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonData,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json',
'Idempotency-Key: ' . $idempotencyKey,
'X-API-Version: 2026-10-15',
'User-Agent: ClicDinamicoBilling/2.0 (PHP 8.4)'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
return ['success' => false, 'error_type' => 'NETWORK_FAILURE'];
}
if ($httpCode === 409) {
// Conflicto = clave de idempotencia ya usada, recuperar resultado previo
return ['success' => true, 'status' => 'IDEMPOTENT_REPLAY', 'note' => 'Suscripción ya creada con esta clave'];
}
if ($httpCode !== 200 && $httpCode !== 201) {
return ['success' => false, 'error_type' => 'GATEWAY_ERROR', 'http_status' => $httpCode];
}
try {
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
return [
'success' => true,
'subscription_id' => $data['id'] ?? '',
'status' => $data['status'] ?? 'unknown',
'current_period_end' => $data['current_period_end'] ?? null
];
} catch (JsonException) {
return ['success' => false, 'error_type' => 'MALFORMED_RESPONSE'];
}
}
}
4. Webhooks Seguros: Verificación HMAC-SHA256
Los webhooks son el mecanismo de notificación asíncrona de Stripe/PayPal. Pero cualquier atacante puede enviar peticiones POST falsas a tu endpoint /webhook/billing. Sin verificación criptográfica, aceptarás eventos fraudulentos que activen suscripciones gratuitas o cancelen cuentas premium.
Toda pasarela seria firma los webhooks con HMAC-SHA256 usando un secreto compartido. Tu backend debe recalcular la firma con el payload crudo recibido y compararla con la cabecera X-Signature usando comparación de tiempo constante (hash_equals) para prevenir ataques de timing.
<?php
// Verificación segura de webhook en endpoint /webhook/billing
$rawBody = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$webhookSecret = getenv('BILLING_WEBHOOK_SECRET') ?: '';
if (empty($webhookSecret)) {
http_response_code(500);
exit(json_encode(['error' => 'Webhook secret no configurado']));
}
$expectedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expectedSignature, $signatureHeader)) {
http_response_code(403);
exit(json_encode(['error' => 'Firma inválida']));
}
// Solo procesar tras verificación exitosa
$event = json_decode($rawBody, true);
// ... lógica de negocio segura ...
5. Reconciliación Contable Asíncrona
El billing recurrente genera eventos financieros que deben reflejarse en sistemas contables externos (ERP, libros mayores). Procesar esta sincronización en el mismo hilo del webhook causa timeouts y pérdida de datos cuando el ERP responde lento.
El patrón correcto es desacoplar recepción de procesamiento:
- Recepción inmediata: Endpoint webhook valida firma, guarda evento crudo en tabla
billing_eventscon estadoPENDING, responde HTTP 200 en <100ms. - Procesamiento asíncrono: Worker en cola (Redis/RabbitMQ) lee eventos pendientes, ejecuta lógica de negocio, actualiza estado a
PROCESSEDoFAILED. - Reintentos automáticos: Eventos fallidos se reencolan con backoff exponencial (1min, 5min, 30min, 2h).
- Auditoría inmutable: Tabla
billing_eventsnunca se borra, solo se actualiza estado. Permite reconstruir historial completo ante disputas.
6. Consideraciones Regulatorias LATAM para Suscripciones
Las suscripciones B2B en Latinoamérica enfrentan requisitos específicos que difieren de mercados anglosajones:
- México (CNBV): Contratos de adhesión digitales deben incluir cláusulas claras de cancelación. Facturación CFDI obligatoria por cada cargo recurrente, no solo al inicio.
- Colombia (Superfinanciera): Autorización expresa escrita/electrónica para débitos recurrentes. Derecho de retracto de 5 días hábiles tras primer cobro.
- Chile (CMF/SERNAC): Notificación previa de 30 días antes de cambios de precio. Cancelación debe ser tan simple como la suscripción (mismo canal).
- Brasil (BACEN/Procon): Renovación automática requiere consentimiento explícito renovable. Prohibido cobrar periodos no autorizados tras cancelación.
Arquitecturalmente, esto implica almacenar metadatos de consentimiento (consent_timestamp, consent_channel, cancellation_deadline) junto a cada suscripción. Pasarelas como dLocal y Ebanx ofrecen APIs adaptadas a estas regulaciones, mientras Stripe/PayPal requieren capas de adaptación local.
7. Conclusión y Recursos Técnicos
La arquitectura de microservicios para billing recurrente no es sobreingeniería: es supervivencia financiera en entornos B2B LATAM. Los patrones de idempotencia, verificación HMAC y procesamiento asíncrono previenen pérdidas directas de ingresos y protegen contra fraudes sistémicos.
Para implementaciones productivas, consulte documentación oficial:
- Stripe Billing Subscriptions Guide: Patrones oficiales de idempotencia y webhooks.
- PayPal Subscriptions API: Gestión de planes y ciclos de vida.
- PCI-DSS v4.0 Requirement 6.4: Seguridad en aplicaciones de pago recurrente.
Nota técnica: Este artículo utiliza marcado Schema.org Article coexistente con el nodo FinancialService corporativo. Los metadatos semánticos se indexan automáticamente vía The SEO Framework.