Checkout embebido
El checkout embebido es un widget de JavaScript que se carga desde un script y se dibuja dentro de una página del comercio. Ahí el pagador elige el país donde va a pagar y deja sus datos; con eso el widget crea una Solicitud de Pago contra la API, abre el checkout de Aloha en otra pestaña y se queda vigilando el Estado hasta que el cobro se completa. La página del comercio no se pierde en ningún momento, y quien integra no escribe una sola llamada a la API.
Conviene saber desde el principio qué parte del cobro ocurre dónde: el widget recoge datos, el checkout de Aloha cobra. Los medios de pago, la cuenta del pagador y el comprobante viven en el checkout, no en la página del comercio. Lo que el widget evita es perder al visitante por el camino y tener que montar un backend para empezar.
Lo que ve el pagador
El widget tiene tres estados y el pagador los recorre sin salir de la página donde está.
- El formulario. Un selector con el país donde se va a pagar y cuatro campos: nombre, apellido, correo y teléfono. Los cinco son obligatorios y el widget los comprueba antes de llamar a la API; los que se hayan precargado con
customeraparecen escritos y se pueden corregir. - La espera. Al continuar, el widget crea la Solicitud de Pago, abre el checkout de Aloha en otra pestaña y se queda esperando. Esa pestaña se abre cuando la API responde, no en el clic mismo, así que el widget deja también un botón para volver a abrirla: es lo que salva el caso de la pestaña cerrada por error o no abierta por el navegador.
- El cobro completado. Cuando el Estado de la Solicitud de Pago pasa a completado, el widget lo muestra y llama a
onPaymentComplete.
Las cuatro llamadas que hace el widget
Ninguna de ellas se escribe: las hace el widget, y las cuatro van con la clave que se le pasó. Las dos primeras no exigen además ningún Permiso; una petición sin clave sí se rechaza en las cuatro.
| Cuándo | Llamada | Permiso que exige |
|---|---|---|
| Al cargar | GET /v1/wallets | ninguno |
| Al cargar | GET /v1/payment-methods | ninguno |
| Al enviar el formulario | POST /v1/payment-links | payment_links:create |
| Cada 3 segundos durante la espera | GET /v1/payment-links/{id} | payment_links:read |
La Solicitud de Pago se crea con source en embedded_checkout, que es lo que permite distinguir después estos cobros de los que se crearon llamando a la API directamente. Y se crea con dos campos que el widget no manda y que por eso toman su valor por omisión, los dos con consecuencias sobre el dinero:
- Sin
amount_type, rigereceive: el monto que se configura está en la moneda de la Billetera y es el importe de destino. Lo que acaba pagando el pagador se calcula a partir de él con la Tasa de Cambio de la moneda que haya elegido, y no se puede fijar desde el widget. - Sin
customer_paid_fee, la Comisión se descuenta de ese importe de destino en vez de sumarse a lo que paga el pagador. Es decir: el monto configurado es sobre lo que se calcula el cobro, no lo que queda acreditado.
La Clave API viaja dentro de la página
Esto es lo que más decide de esta integración. El widget necesita la clave en el navegador para poder llamar a la API desde ahí, así que cualquiera que abra la página puede leerla. No es un descuido que se pueda evitar escribiendo el código de otra manera: es cómo funciona el widget.
De ahí salen tres reglas, y las tres se cumplen desde el panel al crear la clave:
- La clave lleva dos Permisos y ninguno más:
payment_links:createypayment_links:read. No necesita cancelar, no necesita webhooks, y nunca lleva*. Con esos dos, quien copie la clave puede crear Solicitudes de Pago en la cuenta y leer las que conozca por su identificador; con*podría bastante más. - La clave lleva Orígenes Permitidos, con los dominios desde los que se sirve la página. Con la lista vacía no se comprueba nada y la clave funciona igual desde cualquier sitio que la copie.
- Es una clave aparte de la del backend. Una clave con Orígenes Permitidos rechaza toda petición que llegue sin cabecera
Origin, que es el caso de cualquier llamada de servidor a servidor: reutilizar en el widget la clave del backend deja el backend fuera de servicio. Está explicado en Autenticación y Claves API.
Instalación
Una etiqueta de script, desde el CDN:
<script src="https://cdn.alohapay.co/v2/aloha-pay.umd.js"></script>El script expone un único objeto global, AlohaPay, con un método: create.
Crear el widget
<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
</head>
<body>
<div id="aloha-checkout"></div>
<script src="https://cdn.alohapay.co/v2/aloha-pay.umd.js"></script>
<script>
const checkout = AlohaPay.create({
apiKey: 'pay_aBcD1234_live_CLAVE_DE_EJEMPLO',
container: '#aloha-checkout',
amount: 60000,
locale: 'es',
description: 'Reserva de hotel',
defaultCountry: 'CL',
useSandbox: false,
customer: {
firstName: 'Juana',
lastName: 'Pérez',
email: '[email protected]',
phone: '+56912345678'
},
onReady: () => {},
onError: (error) => {},
onPaymentLinkCreated: (checkoutUrl) => {},
onPaymentComplete: () => {}
});
</script>
</body>
</html>create devuelve un objeto con un método destroy, que vacía el contenedor. Y falla de inmediato, lanzando una excepción, en tres casos: si falta apiKey, container o amount; si amount no es un número mayor que cero; y si el selector de container no encuentra ningún elemento. Los tres son errores de programación y no llegan a onError, que existe para los fallos de la API.
De ahí salen las dos reglas para montarlo dentro de un framework, que es lo único que cambia respecto del ejemplo de arriba: el contenedor tiene que existir en el DOM antes de llamar a create —en React o Vue, después del montaje, no durante el render— y al desmontar hay que llamar a destroy. Un segundo create sobre el mismo contenedor no apila nada, lo sustituye; lo que sobrevive a un desmontaje que el widget no vio es la consulta cada tres segundos, que sigue corriendo contra la API.
Las opciones
Tres son obligatorias:
| Opción | Tipo | Qué es |
|---|---|---|
apiKey | cadena | La Clave API de la cuenta, con los Permisos y los Orígenes Permitidos de la sección anterior |
container | cadena | Selector CSS del elemento donde se dibuja el widget |
amount | número | El importe de destino, en la moneda de la Billetera |
Y el resto son opcionales:
| Opción | Tipo | Por omisión | Qué hace |
|---|---|---|---|
locale | en, es o pt | en | Idioma de los textos del widget. Cualquier otro valor cae a inglés |
description | cadena | Payment | La descripción que lleva la Solicitud de Pago |
defaultCountry | cadena | sin valor | Código de país preseleccionado en el selector, como CL o BR. Un código que la API no devuelva se ignora y el selector nace vacío |
useSandbox | booleano | false | Con true, el widget llama a Sandbox en vez de a Producción |
customer | objeto | sin valor | Datos del pagador para precargar el formulario |
styles | objeto | sin valor | Colores y ancho del widget |
Sobre amount, dos cosas que no se ven en la tabla. La primera es que el widget rotula ese monto con la primera moneda que devuelve GET /v1/wallets, y esa lista no viene ordenada: en una cuenta que acredita en más de una moneda, el rótulo de la cabecera puede no ser la moneda en la que se acaba acreditando. La segunda es que hay un piso: una solicitud que convertida vale menos de 2.00 USD se rechaza, y el rechazo llega a onError.
customer
customerLos cuatro campos son opcionales y sirven solo para precargar el formulario; el pagador puede cambiarlos, y los cuatro son obligatorios para él aunque no se manden aquí.
| Campo | Qué es |
|---|---|
firstName | Nombre |
lastName | Apellido |
email | Correo electrónico |
phone | Teléfono, con código de país |
Estos datos no viajan en el cuerpo de la Solicitud de Pago: se le pasan al checkout de Aloha en la dirección que se abre, para que el pagador no vuelva a escribirlos.
styles
styles| Campo | Por omisión | Qué pinta |
|---|---|---|
headerBackground | linear-gradient(135deg, #846ffb 0%, #6b5fcd 100%) | Fondo de la cabecera del monto |
amountColor | #ffffff | Color del monto |
buttonColor | #846ffb | Fondo del botón |
buttonTextColor | #ffffff | Texto del botón |
linkColor | #846ffb | Color de los enlaces |
maxWidth | 480px | Ancho máximo del widget |
Los callbacks
| Callback | Recibe | Cuándo se ejecuta |
|---|---|---|
onReady | nada | Cuando las dos llamadas iniciales han respondido y el formulario ya se puede usar |
onError | el error | Cuando falla una de las dos llamadas iniciales o la creación de la Solicitud de Pago. El mismo texto se le muestra al pagador |
onPaymentLinkCreated | la dirección del checkout | Justo después de crear la Solicitud de Pago y antes de abrir la pestaña. La dirección que recibe lleva ya los datos del pagador añadidos |
onPaymentComplete | nada | Cuando una de las consultas de la espera devuelve el Estado completado |
Los fallos de esas consultas de la espera no llegan a onError: se ignoran para no interrumpir al pagador, y el widget vuelve a intentarlo tres segundos después.
onPaymentComplete no es la confirmación del cobro
onPaymentComplete no es la confirmación del cobroSirve para la experiencia del pagador —mostrarle que su pago llegó— y no para dar por cobrado un pedido. Corre en el navegador del pagador, que es código que el propio pagador puede modificar; y si cierra la pestaña antes de que una consulta devuelva el Estado completado, no se ejecuta nunca aunque el pago se haya hecho.
La confirmación que sí vale se toma en el servidor del comercio, por una de estas dos vías:
- Un endpoint de webhook registrado, suscrito a
payment.completedy con la firma comprobada en cada entrega. Está en Webhooks y en Firmar y verificar webhooks. - Una lectura propia,
GET /v1/payment-links/{id}, hecha desde el servidor con la clave del backend. El identificador está en la dirección que llega aonPaymentLinkCreated: es el último tramo de la ruta, y hay que leerlo antes de los parámetros de consulta, que esa dirección lleva añadidos.
El orden razonable es usar onPaymentComplete para enseñar algo al pagador al instante, y entregar el pedido cuando lo confirme el servidor.
La espera no termina sola
El widget consulta cada tres segundos y solo deja de consultar cuando el Estado es completado. No se rinde por tiempo, y no reacciona a los demás Estados: una Solicitud de Pago cancelada o caducada deja al widget esperando indefinidamente mientras la página siga abierta.
No hay dinero en juego en ese caso —la Solicitud de Pago ya está resuelta del lado de Aloha—, pero sí queda un pagador delante de una pantalla que no avanza. Una página que quiera cerrar ese caso tiene que hacerlo por su cuenta: con onPaymentLinkCreated se tiene el identificador, y con él se puede poner un plazo propio y decidir qué mostrar cuando se agote.
Los países y las monedas los sirve la API
El selector no lleva una lista escrita dentro: la pide a GET /v1/payment-methods cada vez que carga. Hoy devuelve seis países, cada uno con la moneda en la que paga quien elige ese país.
| País | Moneda del pagador |
|---|---|
| Argentina | ARS |
| Bolivia | BOB |
| Brasil | BRL |
| Chile | CLP |
| Colombia | COP |
| México | MXN |
Que la lista venga de la API tiene una consecuencia buena: cuando se añade un país, aparece en el selector sin tocar nada de la página. Y deja a la vista dos rarezas de presentación que no son fallos de la integración: los nombres llegan en inglés y el widget los muestra tal cual, así que en el selector se lee «Brazil» y «Mexico»; y Bolivia sale sin bandera, porque el widget solo trae las de los otros cinco.
El pagador no elige moneda: elige país, y la moneda del país es la que se manda como currency al crear la Solicitud de Pago. USD no está entre ellas, así que un cobro en USD no se hace con este widget.
Sandbox y Producción
useSandbox decide contra cuál de los dos hosts llama el widget, y nada más:
useSandbox | Host |
|---|---|
false, el valor por omisión | https://api.aloha.co/api/external/v1 |
true | https://api.sandbox.aloha.co/api/external/v1 |
La clave tiene que ser de la misma instalación que el host: una clave de Sandbox contra Producción no se resuelve, y al revés tampoco. Las dos instalaciones y lo que cambia al pasar de una a otra están en Ambientes y Sandbox.
Cuando el widget no funciona
- No se dibuja nada. El contenedor no existía cuando se llamó a
create, o el selector no es el del elemento. La consola del navegador lo dice:createlanza la excepción con el nombre de la opción que falla. - Carga y falla en seguida, con un mensaje sobre la clave. Es una de las dos llamadas iniciales. Un
401es la clave —mal copiada, borrada o de la otra instalación— y un403conORIGIN_NOT_ALLOWEDes el origen: el dominio desde el que se sirve la página no está en los Orígenes Permitidos de la clave. Hay que añadirlo con el protocolo completo, y el de desarrollo local también, si se prueba desde ahí. - El formulario se envía y el error habla de verificación. Crear un cobro exige la cuenta verificada, con Verificar Identidad y Verificación Comercial hechas las dos. Mientras falte alguna, la creación se rechaza. Está en Verificación de la cuenta.
- El pago se hizo y el widget sigue esperando. Si el Estado es completado, el widget lo ve en la siguiente consulta; si no avanza, la Solicitud de Pago no está completada. Conviene mirarla desde el servidor antes de dar nada por cobrado.
Cada código de error, con su envoltura y qué hacer con él, está en Errores.
Seguir por aquí
- Inicio rápido — las mismas operaciones, llamadas a mano.
- Autenticación y Claves API — Permisos, Orígenes Permitidos y qué contesta una clave cuando falla.
- Webhooks — la confirmación que sí se puede creer.
- WooCommerce — la otra integración sin escribir contra la API.
Updated 26 days ago
