Saltar al contenido principal

Pagos (Pay In)

Este módulo permite a los Comercios (Merchants) crear órdenes de pago (pay-in) para recibir fondos de sus usuarios o clientes. Las órdenes creadas quedan disponibles para ser procesadas por los Proveedores de Pago integrados en la red de Pago46.

Endpoint base

Todas las rutas descritas a continuación son relativas a la URL base de la API: /api/v1

Checkout de Pago46

Las órdenes contienen el campo redirect_url, que corresponde al enlace de Checkout para continuar el flujo del usuario.

  • Sandbox: https://checkout.dev.pago46.io
  • Producción: https://checkout.prd.pago46.io
  • Formato por orden: https://checkout.{dev|prd}.pago46.io/{UUID}

También puedes abrir la URL base sin UUID para ingresar manualmente el identificador de una orden durante las pruebas de integración en un iframe.

Más detalles en Checkout.

Flujo general

El proceso de pago para comercios se divide en tres etapas principales:

  1. Creación de la orden: El comercio crea una orden de pago especificando el monto, país, datos del pagador y URLs de notificación.
  2. Procesamiento: La orden es procesada por un Proveedor de Pago de la red, quien recibe el efectivo del usuario final.
  3. Notificaciones (webhooks): El comercio recibe el estado final de la orden mediante el webhook configurado.

Requisitos previos

Antes de comenzar, asegúrate de tener:

  • Credenciales de API: Tu Merchant-Key y Merchant-Secret proporcionados por Pago46.
  • Autenticación HMAC: Familiarízate con el esquema de autenticación descrito en la sección de Autenticación.
  • Endpoint de Webhook: Una URL pública HTTPS donde recibirás las notificaciones de cambios de estado.
Seguridad

Todas las peticiones deben incluir las cabeceras de autenticación HMAC: Merchant-Key, Message-Date y Message-Hash.


1. Crear orden de pago

Para iniciar un cobro, debes crear una orden proporcionando la información del monto, país, pagador y configuración de notificaciones.

Endpoint: POST /merchants/orders/pay-in/

Parámetros de la petición

Campos obligatorios

CampoTipoDescripción
order_typeStringTipo de orden: LocalCurrencyOrder
countryStringCódigo ISO del país (ej: MX, CL, AR)
priceDecimalMonto del pago (formato: "1500.00")
price_currencyStringCódigo ISO de la moneda (ej: MXN, CLP, ARS)
descriptionStringDescripción de la transacción
merchant_order_idStringID único de tu sistema (max 127 caracteres)
notify_urlString (URL)URL para recibir webhooks de cambios de estado
return_urlString (URL)URL de retorno para el usuario
expiryDateTimeFecha y hora de expiración (formato ISO 8601)

Campos opcionales

CampoTipoDescripción
consumer_emailStringEmail del pagador
consumer_phone_numberStringTeléfono del pagador (max 128 caracteres)
Datos de contacto

En Pay-In, si envías datos de contacto, debes enviar ambos campos, solo consumer_email o ninguno de los dos. No se acepta consumer_phone_number sin email. El soporte para teléfono sin email estará disponible próximamente.

Límites por país y moneda

Tu cuenta debe estar habilitada para la combinación de country y price_currency que envías; de lo contrario recibes un 400 con el detalle No matching configuration found for merchant, country, and currency.

Además, el price debe estar dentro de los montos mínimo y máximo configurados para tu cuenta en ese país y moneda. Fuera de rango, la API responde 400 con Value is too low. o Value is too high. en el campo price. Revisa los países y monedas disponibles; para tus límites, consulta a tu ejecutivo comercial.

Ejemplo de petición

curl -X POST "https://api.dev.pago46.io/api/v1/merchants/orders/pay-in/" \
-H "Merchant-Key: <TU_MERCHANT_KEY>" \
-H "Message-Date: <TIMESTAMP>" \
-H "Message-Hash: <HMAC_SIGNATURE>" \
-H "Content-Type: application/json" \
-d '{
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Pago de suscripción - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"return_url": "https://tu-comercio.com/pago/volver",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z"
}'

Respuesta exitosa (201 Created)

{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Pago de suscripción - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://tu-comercio.com/pago/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z",
"paid": null
}
Guarda el ID

Guarda el id de la orden devuelto en la respuesta. Lo necesitarás para consultar el estado de la orden posteriormente.


2. Consultar orden

Puedes consultar el estado actual de una orden en cualquier momento usando su ID.

Endpoint: GET /merchants/orders/pay-in/{id}/

Parámetros

ParámetroUbicaciónDescripción
idPathUUID de la orden

Ejemplo de petición

curl -X GET "https://api.dev.pago46.io/api/v1/merchants/orders/pay-in/123e4567-e89b-12d3-a456-426614174000/" \
-H "Merchant-Key: <TU_MERCHANT_KEY>" \
-H "Message-Date: <TIMESTAMP>" \
-H "Message-Hash: <HMAC_SIGNATURE>"

Respuesta (200 OK)

{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Pago de suscripción - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://tu-comercio.com/pago/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z",
"paid": null
}

Estados de la orden

Como comercio solo necesitas reaccionar a los estados finales (COMPLETED, CANCELLED, EXPIRED). Los estados intermedios se resuelven de forma interna y, en Pay-In, no se notifican.

Estado¿Notificado por webhook?¿Qué significa para el comercio?
CREATEDRespuesta al crear la ordenLa orden se ha registrado y está pendiente de procesamiento
READYNo (interno en Pay-In)La orden ya puede ser tomada por un proveedor
PAYMENT_STARTEDNo (interno)Paso intermedio: el usuario está realizando el pago físico en el punto
COMPLETEDSí — estado finalEl pago se completó exitosamente. El proveedor recibió el efectivo del usuario
CANCELLEDSí — estado finalLa orden fue cancelada (por el usuario o por el equipo de Pago46) y no se procesará
EXPIREDSí — estado finalLa orden expiró sin pago y no se procesará

Diagrama de transición de estados


Webhooks (notificaciones)

Cuando la orden alcance un estado final (COMPLETED, CANCELLED o EXPIRED), Pago46 enviará una notificación HTTP POST a la URL especificada en el campo notify_url.

Estructura del webhook

Pago46 enviará el webhook con autenticación HMAC. Debes verificar la firma para asegurar que la notificación proviene de Pago46.

Cabeceras del webhook

POST /webhooks/pago46 HTTP/1.1
Host: tu-comercio.com
Content-Type: application/json
Merchant-Key: <TU_MERCHANT_KEY>
Message-Date: 1704463200.123
Message-Hash: a1b2c3d4e5f6...

Payload del webhook

{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Pago de suscripción - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "COMPLETED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://tu-comercio.com/pago/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z",
"paid": "2026-06-25T14:30:00Z"
}

Verificación de webhooks

Debes verificar la autenticidad de cada webhook recibido para evitar procesar notificaciones fraudulentas.

Ejemplo de verificación en Python

import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

# Tu Merchant Secret (obtenido de Pago46)
MERCHANT_SECRET = "tu_merchant_secret_aqui"

@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
# 1. Extraer headers
merchant_key = request.headers.get('Merchant-Key')
message_date = request.headers.get('Message-Date')
received_hash = request.headers.get('Message-Hash')

# 2. Obtener el body raw
body_str = request.get_data(as_text=True)

# 3. Construir string to sign
# Formato: MERCHANT_KEY:MESSAGE_DATE:METHOD:PATH:BODY
method = request.method # "POST"
path = request.path # "/webhooks/pago46"
string_to_sign = f"{merchant_key}:{message_date}:{method}:{path}:{body_str}"

# 4. Calcular HMAC
calculated_hash = hmac.new(
MERCHANT_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()

# 5. Verificar
if not hmac.compare_digest(calculated_hash, received_hash):
return jsonify({"error": "Invalid signature"}), 403

# 6. Procesar la notificación
order_data = json.loads(body_str)
order_id = order_data.get('id')
order_status = order_data.get('status')
merchant_order_id = order_data.get('merchant_order_id')

print(f"Orden {merchant_order_id} ({order_id}) cambió a estado: {order_status}")

# Actualizar tu base de datos
if order_status == 'COMPLETED':
# Marcar como completada
paid_at = order_data.get('paid')
print(f"Pago completado el: {paid_at}")
# Activar servicio, entregar producto, etc.
elif order_status == 'CANCELLED':
# Marcar como cancelada
print("Pago cancelado")

# 7. Responder con 200 OK
return jsonify({"status": "received"}), 200

if __name__ == '__main__':
app.run(port=5000)

Ejemplo de verificación en Node.js

const express = require('express');
const crypto = require('crypto');
const app = express();

const MERCHANT_SECRET = 'tu_merchant_secret_aqui';

app.post('/webhooks/pago46', express.text({ type: '*/*' }), (req, res) => {
// 1. Extraer headers
const merchantKey = req.headers['merchant-key'];
const messageDate = req.headers['message-date'];
const receivedHash = req.headers['message-hash'];

// 2. Body como string
const bodyStr = req.body;

// 3. Construir string to sign
const method = req.method;
const path = req.path;
const stringToSign = `${merchantKey}:${messageDate}:${method}:${path}:${bodyStr}`;

// 4. Calcular HMAC
const calculatedHash = crypto
.createHmac('sha256', MERCHANT_SECRET)
.update(stringToSign)
.digest('hex');

// 5. Verificar
if (calculatedHash !== receivedHash) {
return res.status(403).json({ error: 'Invalid signature' });
}

// 6. Procesar notificación
const orderData = JSON.parse(bodyStr);
const { id, status, merchant_order_id, paid } = orderData;

console.log(`Orden ${merchant_order_id} (${id}) cambió a estado: ${status}`);

if (status === 'COMPLETED') {
console.log(`Pago completado el: ${paid}`);
// Activar servicio, entregar producto, etc.
} else if (status === 'CANCELLED') {
console.log('Pago cancelado');
}

// 7. Responder
res.status(200).json({ status: 'received' });
});

app.listen(5000, () => {
console.log('Webhook server listening on port 5000');
});
Respuesta al webhook

Debes responder con un código HTTP 200 o 201 para confirmar la recepción. Si no respondes exitosamente, Pago46 reintentará enviar la notificación.


Errores comunes

Consulta la sección Manejo de errores comunes para ejemplos y el formato estándar de respuestas de error para validaciones de límites, campos obligatorios y otros errores de negocio.

Validación de campos

Si envías datos inválidos o incompletos, recibirás un error 400 Bad Request con detalles específicos:

{
"type": "validation_error",
"errors": [
{
"code": "invalid",
"detail": "No matching configuration found for merchant, country, and currency.",
"attr": "merchant_country_order_setting"
},
{
"code": "required",
"detail": "This field is required.",
"attr": "country"
},
{
"code": "required",
"detail": "This field is required.",
"attr": "price"
},
{
"code": "required",
"detail": "This field is required.",
"attr": "price_currency"
}
]
}

Tabla de errores HTTP

Código HTTPDescripciónSolución
400 Bad RequestDatos inválidos o campos faltantesVerifica que todos los campos obligatorios estén presentes y con el formato correcto
403 ForbiddenAutenticación fallidaVerifica tus credenciales y la firma HMAC
404 Not FoundOrden no encontradaVerifica que el ID de la orden sea correcto
422 Unprocessable EntityError de lógica de negocioRevisa el mensaje de error específico

Buenas prácticas

1. Identificador de orden

Asigna un merchant_order_id único por operación. La API aplica una restricción de unicidad sobre (merchant_order_id, merchant): si creas otra orden con un merchant_order_id ya utilizado, la petición se rechaza en lugar de duplicar la orden. Úsalo para conciliar cada orden con tu sistema. Si no estás seguro de si una creación se completó, consulta la orden en lugar de reintentar el POST.

2. Manejo de webhooks

  • Procesa webhooks de forma asíncrona: No bloquees la respuesta HTTP mientras procesas la lógica de negocio.
  • Implementa reintentos: Si tu servidor webhook está caído, Pago46 reintentará el envío.
  • Valida siempre la firma HMAC: Nunca confíes en webhooks sin verificar su autenticidad.

3. Expiración de órdenes

Configura expiry entre 24 y 72 horas después de crear la orden. Este rango reduce el riesgo de que un usuario conserve una captura del código e intente pagar cuando la operación ya no debería estar vigente. Evita órdenes con varios días o semanas de validez.

4. Monitoreo de órdenes

  • Consulta las órdenes que no alcancen un estado final dentro del tiempo esperado.
  • Implementa alertas para detectar órdenes pendientes por demasiado tiempo.
  • Revisa la página de Estado del Servicio ante errores atípicos para confirmar si hay incidentes o mantenimientos activos.

5. URLs de notificación

  • Usa URLs HTTPS para notify_url.
  • Asegúrate de que el endpoint esté siempre disponible.
  • Implementa registros para facilitar la depuración.

Ejemplo completo de integración

A continuación, un ejemplo completo en Python que muestra cómo crear una orden y manejar webhooks:

import hmac
import hashlib
import time
import requests
import json
from flask import Flask, request, jsonify

# Configuración
API_BASE_URL = "https://api.dev.pago46.io"
MERCHANT_KEY = "tu_merchant_key"
MERCHANT_SECRET = "tu_merchant_secret"

app = Flask(__name__)

def generate_hmac(method, path, body_dict=None):
"""Genera la firma HMAC para autenticación"""
timestamp = str(time.time())
body_str = json.dumps(body_dict) if body_dict else ""

string_to_sign = f"{MERCHANT_KEY}:{timestamp}:{method}:{path}:{body_str}"

signature = hmac.new(
MERCHANT_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()

return {
"Merchant-Key": MERCHANT_KEY,
"Message-Date": timestamp,
"Message-Hash": signature,
"Content-Type": "application/json"
}

def create_payin_order(amount, user_email, user_phone, merchant_order_id):
"""Crea una orden de pago"""
path = "/api/v1/merchants/orders/pay-in/"

order_data = {
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": str(amount),
"price_currency": "MXN",
"description": f"Pago de usuario {user_email}",
"merchant_order_id": merchant_order_id,
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"return_url": "https://tu-comercio.com/pago/volver",
"consumer_email": user_email,
"consumer_phone_number": user_phone,
"expiry": "2026-06-27T23:59:59Z"
}

headers = generate_hmac("POST", path, order_data)

response = requests.post(
f"{API_BASE_URL}{path}",
headers=headers,
json=order_data
)

if response.status_code == 201:
order = response.json()
print(f"Orden creada exitosamente: {order['id']}")
return order
else:
print(f"Error al crear orden: {response.status_code}")
print(response.text)
return None

@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
"""Maneja webhooks de Pago46"""
# Verificar firma HMAC
merchant_key = request.headers.get('Merchant-Key')
message_date = request.headers.get('Message-Date')
received_hash = request.headers.get('Message-Hash')
body_str = request.get_data(as_text=True)

string_to_sign = f"{merchant_key}:{message_date}:{request.method}:{request.path}:{body_str}"
calculated_hash = hmac.new(
MERCHANT_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()

if not hmac.compare_digest(calculated_hash, received_hash):
return jsonify({"error": "Invalid signature"}), 403

# Procesar webhook
order = json.loads(body_str)

print(f"Webhook recibido para orden: {order['merchant_order_id']}")
print(f" Estado: {order['status']}")

# Los webhooks para comercios solo contienen estados finales
if order['status'] == 'COMPLETED':
print(f" Pago completado el {order['paid']}")
# Activar servicio, entregar producto, etc.
elif order['status'] == 'CANCELLED':
print(" Pago cancelado")
elif order['status'] == 'EXPIRED':
print(" Orden expirada")

return jsonify({"status": "received"}), 200

if __name__ == '__main__':
# Ejemplo: Crear una orden de pago
order = create_payin_order(
amount=1500.00,
user_email="usuario@ejemplo.com",
user_phone="+525512345678",
merchant_order_id="ORDER-2024-" + str(int(time.time()))
)

# Iniciar servidor de webhooks
print("\nIniciando servidor de webhooks...")
app.run(port=5000)

Próximos pasos

Soporte

Si tienes preguntas o necesitas ayuda con tu integración, contacta a tu ejecutivo comercial o escríbenos a contacto@pago46.com.