Integración de app web
Dispara pagos NFC en la app MONEI Pay desde un POS basado en navegador (una pantalla de pedidos web, un POS en tablet, un enlace de pago de back-office). Tu página web abre el enlace de MONEI Pay; MONEI Pay gestiona el pago por contacto; el resultado vuelve a tu backend mediante un webhook firmado y al navegador mediante una redirección.
No hay SDK que instalar — esta es una integración pura de HTTP + deep link. Úsala cuando tu POS es HTML/JS y no tienes una app móvil nativa propia.
Abre https://pay.monei.com/accept-payment — un Universal Link en iOS y un App Link en Android. Lanza MONEI Pay directamente, sin la confirmación "¿Abrir en MONEI Pay?", en ambas plataformas. El esquema de URL heredado monei-pay://accept-payment acepta los mismos parámetros de consulta y sigue funcionando, pero muestra la hoja de confirmación en iOS — prefiere el enlace HTTPS. Si MONEI Pay no está instalado, el enlace HTTPS abre el inicio de sesión de la app web de MONEI Pay en lugar de fallar silenciosamente.
Cómo funciona
- El navegador pide a tu backend que inicie un pago.
- Tu backend genera un token de autenticación POS, construye el deep link con
callback_url(tu webhook) ycomplete_url(donde Safari devuelve al usuario) y lo retorna al navegador. - El navegador abre el deep link. El Universal Link lanza MONEI Pay directamente — sin confirmación de cambio de app.
- El usuario acerca su tarjeta al dispositivo que ejecuta MONEI Pay.
- MONEI envía un webhook firmado a tu
callback_url— este es el resultado autoritativo. Completa el pedido aquí. - MONEI Pay abre
complete_urlen el navegador original; tu página lee los parámetros de consulta del resultado y luego consulta tu backend para confirmar.
Los dos canales son independientes a propósito: el webhook es el registro de confianza (HMAC MONEI-Signature), la redirección es un detalle de UX que puede suplantarse. Confirma siempre mediante el webhook o Obtener pago antes de completar el pedido. Consulta Entrega de resultados.
Requisitos previos
- Cuenta MONEI y clave de API
- Un dispositivo con MONEI Pay (el iPhone / Android del comercio — el dispositivo que físicamente acerca la tarjeta)
- Un backend capaz de generar tokens de autenticación POS y alojar el webhook
callback_url - Un endpoint HTTPS bajo tu control (obligatorio para
callback_url— las IPs locales yhttp://están bloqueadas)
Paso 1: Endpoint del lado del servidor
Tu backend expone un pequeño endpoint que el navegador llama para iniciar un pago. Genera el token de autenticación, persiste un registro de pago y devuelve el deep link.
- Node.js
- Python
- PHP
import express from 'express';
import {Monei} from '@monei-js/node-sdk';
import {randomUUID} from 'crypto';
const app = express();
app.use(express.json());
const monei = new Monei('YOUR_API_KEY');
app.post('/create-payment', async (req, res) => {
const {amount, orderId} = req.body;
const {token} = await monei.posAuthToken.create({});
// Store a row so the webhook handler and /payment-status can find it
const paymentId = randomUUID();
await db.payments.insert({id: paymentId, orderId, status: 'PENDING'});
const params = new URLSearchParams({
amount: String(amount),
auth_token: token,
order_id: orderId,
callback_url: `https://merchant.com/webhooks/monei`,
complete_url: `https://merchant.com/checkout/done?paymentId=${paymentId}`
});
res.json({
deepLink: `https://pay.monei.com/accept-payment?${params.toString()}`,
paymentId
});
});
from flask import Flask, request, jsonify
from urllib.parse import urlencode
import Monei
import uuid
app = Flask(__name__)
monei = Monei.MoneiClient(api_key="YOUR_API_KEY")
@app.post("/create-payment")
def create_payment():
body = request.get_json()
token_result = monei.pos_auth_token.create()
payment_id = str(uuid.uuid4())
db.payments.insert(id=payment_id, order_id=body["orderId"], status="PENDING")
params = urlencode({
"amount": body["amount"],
"auth_token": token_result.token,
"order_id": body["orderId"],
"callback_url": "https://merchant.com/webhooks/monei",
"complete_url": f"https://merchant.com/checkout/done?paymentId={payment_id}"
})
return jsonify({
"deepLink": f"https://pay.monei.com/accept-payment?{params}",
"paymentId": payment_id
})
<?php
require_once 'vendor/autoload.php';
use Monei\MoneiClient;
$monei = new MoneiClient('YOUR_API_KEY');
$body = json_decode(file_get_contents('php://input'), true);
$tokenResult = $monei->posAuthToken->create([]);
$paymentId = bin2hex(random_bytes(16));
// $db->payments->insert([...]) — persist for webhook + status lookup
$params = http_build_query([
'amount' => $body['amount'],
'auth_token' => $tokenResult->getToken(),
'order_id' => $body['orderId'],
'callback_url' => 'https://merchant.com/webhooks/monei',
'complete_url' => "https://merchant.com/checkout/done?paymentId={$paymentId}"
]);
echo json_encode([
'deepLink' => "https://pay.monei.com/accept-payment?{$params}",
'paymentId' => $paymentId
]);
?>
order_id para la conciliaciónPasa tu propio order_id para que aparezca como orderId del pago en el webhook firmado callback_url. Evita la consulta adicional a la base de datos al correlacionar con tu pedido POS. Si se omite, MONEI genera uno. Consulta Conciliación.
Paso 2: Abre el deep link desde el navegador
Cuando el cajero hace clic en Cobrar en tu POS web, obtén el deep link y navega hacia él.
<button id="charge">Charge €15.00</button>
<script>
document.getElementById('charge').addEventListener('click', async () => {
const res = await fetch('/create-payment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({amount: 1500, orderId: 'order_42'})
});
const {deepLink, paymentId} = await res.json();
// Persist locally so the return page can find the right payment
sessionStorage.setItem('pendingPaymentId', paymentId);
// Hand off to MONEI Pay. The Universal Link opens the app directly — no prompt.
window.location.href = deepLink;
});
</script>
Safari y Chrome solo permiten la navegación con esquema de app cuando ocurre dentro de un gestor de clic real (o pointerdown / touchend). No abras el deep link desde setTimeout, callbacks de red o carga de página — el navegador lo bloqueará como popup.
Comportamiento del navegador
- El Universal Link abre MONEI Pay directamente — sin la hoja de confirmación "¿Abrir en MONEI Pay?" en iOS, y sin selector de app en Android.
- Si MONEI Pay no está instalado (o ejecuta una versión anterior a la compatibilidad con Universal Links), el enlace abre el inicio de sesión de la app web de MONEI Pay en
pay.monei.comen lugar de fallar silenciosamente. El cajero puede instalar la app desde ahí. - El esquema heredado
monei-pay://accept-paymentacepta los mismos parámetros pero, en iOS, Safari muestra una hoja de confirmación la primera vez que un dominio lo activa — otra razón para preferir el enlace HTTPS. - Un
<iframe>embebido no puede navegar la ubicación de nivel superior a MONEI Pay. Si tu POS se ejecuta dentro de un iframe, abre el enlace mediantewindow.top.location.href = ...(requiere mismo origen oallow-top-navigation).
Paso 3: Gestiona el retorno en complete_url
Tras completar (o cancelar) el pago por contacto, MONEI Pay abre complete_url en el mismo navegador. Trata sus parámetros de consulta solo como una pista — confirma mediante el webhook o Obtener pago.
<script>
const params = new URLSearchParams(location.search);
const paymentId = params.get('paymentId');
if (params.get('success') === 'true') {
// Optimistically show "processing"; confirm from your backend below
document.body.innerHTML = '<p>Confirming payment…</p>';
} else {
document.body.innerHTML = ``;
}
// Poll your backend — the webhook is the source of truth
async function poll() {
const res = await fetch(`/payment-status/${paymentId}`);
const {status} = await res.json();
if (status === 'COMPLETED') {
document.body.innerHTML = '<p>Paid ✓</p>';
} else if (status === 'FAILED') {
document.body.innerHTML = '<p>Payment failed</p>';
} else {
setTimeout(poll, 1000);
}
}
if (params.get('success') === 'true') poll();
</script>
Los parámetros de consulta de complete_url están documentados en Entrega de resultados.
Paso 4: Recibe y verifica el webhook
El callback_url que configuraste en el Paso 1 recibe un POST firmado cuando finaliza el pago. Verifica el HMAC MONEI-Signature antes de confiar en la carga útil.
import {Monei} from '@monei-js/node-sdk';
const monei = new Monei('YOUR_API_KEY');
app.post('/webhooks/monei', express.raw({type: '*/*'}), async (req, res) => {
const signature = req.headers['monei-signature'];
let payment;
try {
payment = monei.verifySignature(req.body.toString('utf8'), signature);
} catch {
return res.status(401).send('Invalid signature');
}
// payment.orderId is what you sent as order_id in the deep link
await db.payments.update(
{orderId: payment.orderId},
{status: payment.status, monei_id: payment.id}
);
res.status(200).send('OK');
});
Guía completa de verificación de firma: Verificar firma.
Resumen del modelo de confianza
| Canal | Dirección | Confianza | Usar para |
|---|---|---|---|
callback_url | Servidor → servidor | De confianza | Completar pedidos, liquidación, lógica de negocio |
complete_url | Redirección navegador | NO de confianza | Solo retroalimentación UX — confirma mediante el webhook antes |
Nunca completes un pedido basándote únicamente en los parámetros de consulta de complete_url. El cajero puede construir cualquier URL.
Problemas frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
| Safari no hace nada al hacer clic | Navegación no dentro de un gesto de usuario, o bloqueador de popups | Dispara desde un evento click / pointer; evita setTimeout / async-then-navigate |
| El enlace abre el inicio de sesión web de MONEI Pay, no la app | MONEI Pay no está instalado (o una versión anterior a Universal Link) en este dispositivo | Instala o actualiza MONEI Pay desde el App Store / Play Store en el dispositivo que acerca la tarjeta |
| El webhook nunca se dispara | callback_url es http://, una IP privada o supera los 2048 caracteres | Usa una URL https:// pública. El cliente con DNS fijo de MONEI también bloquea RFC1918 / loopback / IMDS |
El webhook se dispara pero falta mi order_id | El cajero usa una versión antigua de MONEI Pay (anterior a 2.7.3) que no analizaba order_id | Asegúrate de que el dispositivo ejecuta la última versión de MONEI Pay desde la tienda |
| Dos pestañas del navegador saltan a MONEI Pay | Ambas abrieron el deep link en el mismo grupo de ventana | Usa una sola pestaña POS; el deep link debe estar vinculado a un único pago en vuelo |
complete_url se abre pero perdió la sesión | Safari la abre en una pestaña nueva sin tus cookies | Lleva el estado en la URL (query paymentId) y confirma mediante tu backend en lugar de depender de la sesión |
El usuario recarga la página complete_url | Idempotencia de tu lado | El endpoint /payment-status/{id} del servidor debe ser seguro de llamar repetidamente |
Referencia
- Lista completa de parámetros del deep link: Integración manual iOS (la URL es idéntica en todas las plataformas)
- Modelo de confianza y parámetros de consulta de
complete_url: Entrega de resultados - Firma del webhook: Verificar firma
- APIs del servidor usadas aquí: Crear token de autenticación POS, Obtener pago