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 deep link 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 + esquema de URL. Úsala cuando tu POS es HTML/JS y no tienes una app móvil nativa propia.
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. iOS / Android solicita al usuario que cambie a MONEI Pay.
- 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: `monei-pay://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"monei-pay://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' => "monei-pay://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. On iOS Safari prompts to switch apps.
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.
Particularidades de Safari
- Safari muestra una hoja de confirmación ("¿Abrir en MONEI Pay?") la primera vez que un dominio activa
monei-pay://. El usuario debe pulsar Abrir. - Si MONEI Pay no está instalado, Safari permanece en tu página silenciosamente. No verás ningún evento de error. Detéctalo con un temporizador de reserva:
window.location.href = deepLink;
setTimeout(() => {
// Page is still visible → user dismissed the sheet or MONEI Pay isn't installed
if (!document.hidden) {
alert('MONEI Pay does not appear to be installed on this device.');
}
}, 2000);
- Un
<iframe>embebido no puede navegar la ubicación de nivel superior a un esquema personalizado. Si tu POS se ejecuta dentro de un iframe, abre el deep link 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 navegador muestra "No se puede abrir la página" | MONEI Pay no está instalado en este dispositivo | Instala 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