# 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[​](#cómo-funciona "Enlace directo al Cómo funciona")

<!-- -->

1. El navegador pide a **tu backend** que inicie un pago.
2. Tu backend genera un [token de autenticación POS](https://docs.monei.com/es/es/monei-pay/app-integration/getting-started/.md#step-2-generate-a-pos-auth-token), construye el deep link con `callback_url` (tu webhook) y `complete_url` (donde Safari devuelve al usuario) y lo retorna al navegador.
3. El navegador abre el deep link. iOS / Android solicita al usuario que cambie a MONEI Pay.
4. El usuario acerca su tarjeta al dispositivo que ejecuta MONEI Pay.
5. MONEI envía un **webhook firmado** a tu `callback_url` — este es el resultado autoritativo. Completa el pedido aquí.
6. MONEI Pay abre `complete_url` en 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](https://docs.monei.com/es/es/apis/rest/payments-get/.md) antes de completar el pedido.** Consulta [Entrega de resultados](https://docs.monei.com/es/es/monei-pay/app-integration/getting-started/.md#result-delivery).

## Requisitos previos[​](#requisitos-previos "Enlace directo al Requisitos previos")

* [Cuenta MONEI](https://dashboard.monei.com/register) 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 y `http://` están bloqueadas)

## Paso 1: Endpoint del lado del servidor[​](#paso-1-endpoint-del-lado-del-servidor "Enlace directo al 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

server.js

```
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

  });

});
```

server.py

```
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

    })
```

server.php

```
<?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ón

Pasa 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](https://docs.monei.com/es/es/monei-pay/app-integration/getting-started/.md#reconciliation-with-order_id).

## Paso 2: Abre el deep link desde el navegador[​](#paso-2-abre-el-deep-link-desde-el-navegador "Enlace directo al 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.

checkout.html

```
<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>
```

Dispara desde un gesto del usuario

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[​](#particularidades-de-safari "Enlace directo al 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 mediante `window.top.location.href = ...` (requiere mismo origen o `allow-top-navigation`).

## Paso 3: Gestiona el retorno en `complete_url`[​](#paso-3-gestiona-el-retorno-en-complete_url "Enlace directo al 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](https://docs.monei.com/es/es/apis/rest/payments-get/.md).

checkout/done.html

```
<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 = `<p>Payment failed: ${params.get('error')}</p>`;

  }



  // 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](https://docs.monei.com/es/es/monei-pay/app-integration/getting-started/.md#result-query-parameters-on-complete_url-open).

## Paso 4: Recibe y verifica el webhook[​](#paso-4-recibe-y-verifica-el-webhook "Enlace directo al 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.

webhook-handler.js

```
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](https://docs.monei.com/es/es/guides/verify-signature/.md).

## Resumen del modelo de confianza[​](#resumen-del-modelo-de-confianza "Enlace directo al 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[​](#problemas-frecuentes "Enlace directo al 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[​](#referencia "Enlace directo al Referencia")

* Lista completa de parámetros del deep link: [Integración manual iOS](https://docs.monei.com/es/es/monei-pay/app-integration/ios/.md#url-parameters) (la URL es idéntica en todas las plataformas)
* Modelo de confianza y parámetros de consulta de `complete_url`: [Entrega de resultados](https://docs.monei.com/es/es/monei-pay/app-integration/getting-started/.md#result-delivery)
* Firma del webhook: [Verificar firma](https://docs.monei.com/es/es/guides/verify-signature/.md)
* APIs del servidor usadas aquí: [Crear token de autenticación POS](https://docs.monei.com/es/es/apis/rest/pos-auth-token-create/.md), [Obtener pago](https://docs.monei.com/es/es/apis/rest/payments-get/.md)
