Por qué integrar OXXO no es como integrar cualquier pasarela de pago
OXXO es async: el cliente paga en tienda, el dinero llega horas después. Te explico el flujo completo, el error que provoca churn silencioso, y cómo manejarlo correctamente.
Cuando un cliente pide que integres OXXO como método de pago, la primera reacción suele ser: "¿cuánto puede costar agregar un método más?" La respuesta es: bastante más de lo que parece, porque OXXO no funciona como ninguna otra pasarela. No es sincrónico. No hay autorización al momento del checkout. Y el error que más veo — no entregar la referencia al cliente después de crear el pago — provoca churn silencioso: el cliente no puede pagar y simplemente abandona.
OXXO en números
OXXO es la cadena de tiendas de conveniencia más grande de México, con más de 22,000 sucursales — más que todos los Walmart de México y Centroamérica juntos. Acepta pagos de servicios, facturas, y compras en línea con efectivo. Para una porción significativa de la población mexicana, es la forma de pagar en internet: no tienen tarjeta de crédito, o prefieren no usarla en línea, pero viven a menos de 10 minutos de un OXXO.
Si no ofreces OXXO como opción, estás excluyendo a una parte del mercado que paga igual de bien, pero prefiere pagar diferente.
El error que provoca churn silencioso
Para pagar en OXXO, el cliente necesita un número de referencia. La pasarela (MercadoPago, Conekta, OpenPay) genera ese número cuando creas el intent de pago. Tu sistema tiene que mostrarle ese número al cliente antes de que cierre la ventana del checkout.
El error que veo frecuentemente: el backend crea el intent, almacena la referencia en la base de datos, y redirige al cliente a una página de "tu pedido fue recibido" — sin mostrarle la referencia. El cliente no tiene cómo pagar. No sabe que hay un número de referencia. El pago nunca llega. El pedido queda en estado "pendiente" hasta que expira.
El churn es silencioso porque el cliente no "falló" en el checkout — llegó a la página de confirmación. Tu tasa de conversión muestra que completaron el checkout. Pero no pagaron. La diferencia entre pago iniciado y pago confirmado es grande con OXXO, y casi siempre se explica por este error.
// Se crea el intent correctamente...
$intent = $gateway->createOxxoPayment([
'amount' => 50000, // centavos
'currency' => 'MXN',
'expires_at' => now()->addHours(48),
]);
$order->update(['payment_reference' => $intent->reference()]);
// ❌ Se redirige SIN mostrar la referencia al cliente
return redirect('/pedido/confirmado'); // El cliente no sabe que tiene que ir a OXXO
// Siempre redirigir a una página que muestre la referencia OXXO
return redirect('/pedido/pagar-en-oxxo')->with([
'referencia' => $intent->reference(), // el número de barcode
'monto' => '$500.00 MXN',
'vence' => 'en 48 horas',
'instrucciones' => 'Muestra este número en cualquier OXXO',
]);
// Con ForgeStack — el VoucherUrl apunta a una página pre-construida:
$charge = $gateway->charge(OxxoPaymentRequest::make(
amount: Money::mxn(50000),
expiresIn: Duration::hours(48),
));
// $charge->voucherUrl() → URL del voucher listo para mostrar
Verificación del webhook — no opcional
La pasarela notifica el pago a tu endpoint via HTTP POST. Cualquiera puede hacer un POST a tu endpoint. Tienes que verificar que la notificación viene realmente de la pasarela, no de alguien que quiere que liberes un pedido sin haber pagado.
Cada pasarela implementa la verificación diferente. MercadoPago usa una firma HMAC-SHA256 sobre el body con tu API key. Conekta usa una firma sobre la cadena timestamp.rawBody. Si no verificas, cualquiera puede hacer POST a tu endpoint y liberar pedidos gratis.
// Con ForgeStack — la verificación está abstraída por pasarela:
try {
$event = $gateway->parseWebhook(
payload: $request->getContent(),
headers: $request->headers->all(),
);
} catch (WebhookSignatureException $e) {
return response('', 400); // firma inválida — rechazar
}
if ($event->type() === PaymentEventType::Paid) {
$order->markAsPaid($event->chargeId());
}
Expiración de referencias — no la ignores
Las referencias OXXO expiran. Si el cliente no paga antes del vencimiento, la referencia ya no es válida. Tu sistema necesita manejar esto: limpiar los pedidos expirados, notificar al cliente que puede volver a generar una referencia si todavía quiere el producto, y liberar el inventario reservado si corresponde.
// Symfony Messenger / Laravel Queue — correr cada hora
class ExpireOxxoPendingPaymentsHandler
{
public function __invoke(ExpireOxxoPendingPaymentsMessage $msg): void
{
$expired = $this->repo->findExpiredOxxoPending();
foreach ($expired as $payment) {
$payment->expire();
$this->events->dispatch(new OxxoPaymentExpired($payment));
}
}
}
En resumen: OXXO es un paradigma diferente
A diferencia de un pago con tarjeta que se autoriza o rechaza en segundos, un pago OXXO tiene un ciclo de vida que puede durar horas o días. Tu sistema necesita estar diseñado para eso desde el principio: estados explícitos (pendiente, pagado, expirado, cancelado), entrega de referencias al cliente sin excepción, verificación de webhooks, y limpieza de referencias vencidas.
Una vez que diseñas con estos estados desde el principio, la integración no es difícil. El problema es cuando se intenta encajar OXXO en una arquitectura de pagos sincrónica que no fue diseñada para recibirlo.
ForgeStack · Pagos LATAM para PHP
OXXO, SPEI y MercadoPago — ya implementados
ForgeStack LATAM Pay abstrae OXXO, SPEI, tarjeta y más a través de una sola interfaz PHP. Verificación de webhooks, manejo de expiración, estado pending→paid — todo incluido. Bridges para Laravel y Symfony.
Ver LATAM Pay Bundle