Retiros (Pay Out)
Este módulo permite a los Comercios (Merchants) crear órdenes de retiro (pay-out) para dispersar fondos a sus usuarios o clientes. Las órdenes creadas quedan disponibles para ser procesadas por los Proveedores de Pago integrados en la red de Pago46.
Todas las rutas descritas a continuación son relativas a la URL base de la API: /api/v1
Flujo General
El proceso de retiro para comercios se divide en tres etapas principales:
- Creación de Orden: El comercio crea una orden de retiro especificando el monto, país, datos del beneficiario y URLs de notificación.
- Procesamiento: La orden es procesada por un Proveedor de Pago de la red, quien entrega el efectivo al usuario final.
- Notificaciones (Webhooks): El comercio recibe actualizaciones del estado de la orden a través de webhooks configurados.
Requisitos Previos
Antes de comenzar, asegúrate de tener:
- Credenciales de API: Tu
Provider-KeyyProvider-Secretproporcionados 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.
Todas las peticiones deben incluir los headers de autenticación HMAC: Provider-Key, Message-Date y Message-Hash.
1. Crear Orden de Retiro
Para iniciar un retiro, debes crear una orden proporcionando la información del monto, país, beneficiario y configuración de notificaciones.
Endpoint: POST /merchants/orders/pay-out/
Parámetros de Request
Campos Obligatorios
| Campo | Tipo | Descripción |
|---|---|---|
order_type | String | Tipo de orden: LocalCurrencyOrder |
country | String | Código ISO del país (ej: MX, CL, CO) |
price | Decimal | Monto del retiro (formato: "1500.00") |
description | String | Descripción de la transacción |
merchant_order_id | String | ID único de tu sistema (max 127 caracteres) |
notify_url | String (URL) | URL para recibir webhooks de cambios de estado |
redirect_url | String (URL) | URL de redirección después del proceso |
return_url | String (URL) | URL de retorno para el usuario |
expiry | DateTime | Fecha y hora de expiración (formato ISO 8601) |
Campos Opcionales
| Campo | Tipo | Descripción |
|---|---|---|
consumer_email | String | Email del beneficiario |
consumer_phone_number | String | Teléfono del beneficiario (max 128 caracteres) |
Para retiros internacionales o con conversión de divisas, consulta la documentación de Comercios con Moneda Extranjera en el menú lateral.
Ejemplo de Request
curl -X POST "https://api.sandbox.pago46.io/api/v1/merchants/orders/pay-out/" \
-H "Provider-Key: <TU_PROVIDER_KEY>" \
-H "Message-Date: <TIMESTAMP>" \
-H "Message-Hash: <HMAC_SIGNATURE>" \
-H "Content-Type: application/json" \
-d '{
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z"
}'
Respuesta Exitosa (201 Created)
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
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-out/{id}/
Parámetros
| Parámetro | Ubicación | Descripción |
|---|---|---|
id | Path | UUID de la orden |
Ejemplo de Request
curl -X GET "https://api.sandbox.pago46.io/api/v1/merchants/orders/pay-out/123e4567-e89b-12d3-a456-426614174000/" \
-H "Provider-Key: <TU_PROVIDER_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": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "READY",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
Estados de la Orden
Cada orden de retiro pasa por diferentes estados durante su ciclo de vida. Es importante que tu sistema maneje correctamente cada uno de estos estados.
| Estado | Descripción | ¿Qué significa para el comercio? |
|---|---|---|
CREATED | Orden creada exitosamente | La orden se ha registrado y está pendiente de asignación a un proveedor |
READY | Lista para procesar | Un proveedor puede comenzar a procesar el retiro |
PAYMENT_STARTED | Pago en proceso | Un proveedor ha bloqueado la orden y está procesando el retiro |
COMPLETED | Completada | El retiro se completó exitosamente. El beneficiario recibió el efectivo |
CANCELLED | Cancelada | La orden fue cancelada y no se procesará |
Diagrama de Transición de Estados
Webhooks (Notificaciones)
Cada vez que el estado de una orden cambia, Pago46 enviará una notificación HTTP POST a la URL especificada en el campo notify_url de tu orden.
Estructura del Webhook
El webhook será enviado con autenticación HMAC. Debes verificar la firma para asegurar que la notificación proviene de Pago46.
Headers del Webhook
POST /webhooks/pago46 HTTP/1.1
Host: tu-comercio.com
Content-Type: application/json
Provider-Key: PAGO46_SYSTEM
Message-Date: 1704463200.123
Message-Hash: a1b2c3d4e5f6...
Payload del Webhook
- Estado: READY
- Estado: PAYMENT_STARTED
- Estado: COMPLETED
- Estado: CANCELLED
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "READY",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "PAYMENT_STARTED",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "COMPLETED",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": "2024-01-15T14:30:00Z"
}
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Retiro de saldo - Usuario ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CANCELLED",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"consumer_email": "usuario@ejemplo.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
Verificación de Webhooks
Es crítico que verifiques la autenticidad de cada webhook recibido para evitar procesamiento de notificaciones fraudulentas.
Ejemplo de Verificación en Python
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
# Tu Provider Secret (obtenido de Pago46)
PROVIDER_SECRET = "tu_provider_secret_aqui"
@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
# 1. Extraer headers
provider_key = request.headers.get('Provider-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: PROVIDER_KEY:MESSAGE_DATE:METHOD:PATH:BODY
method = request.method # "POST"
path = request.path # "/webhooks/pago46"
string_to_sign = f"{provider_key}:{message_date}:{method}:{path}:{body_str}"
# 4. Calcular HMAC
calculated_hash = hmac.new(
PROVIDER_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"Retiro completado el: {paid_at}")
elif order_status == 'CANCELLED':
# Marcar como cancelada
print("Retiro 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 PROVIDER_SECRET = 'tu_provider_secret_aqui';
app.post('/webhooks/pago46', express.text({ type: '*/*' }), (req, res) => {
// 1. Extraer headers
const providerKey = req.headers['provider-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 = `${providerKey}:${messageDate}:${method}:${path}:${bodyStr}`;
// 4. Calcular HMAC
const calculatedHash = crypto
.createHmac('sha256', PROVIDER_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(`Retiro completado el: ${paid}`);
// Actualizar base de datos
} else if (status === 'CANCELLED') {
console.log('Retiro cancelado');
// Actualizar base de datos
}
// 7. Responder
res.status(200).json({ status: 'received' });
});
app.listen(5000, () => {
console.log('Webhook server listening on port 5000');
});
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
Validación de Campos
Si envías datos inválidos o incompletos, recibirás un error 400 Bad Request con detalles específicos:
{
"country": [
"This field is required."
],
"price": [
"A valid number is required."
],
"expiry": [
"Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]."
]
}
Tabla de Errores HTTP
| Código HTTP | Descripción | Solución |
|---|---|---|
400 Bad Request | Datos inválidos o campos faltantes | Verifica que todos los campos obligatorios estén presentes y con el formato correcto |
403 Forbidden | Autenticación fallida | Verifica tus credenciales y la firma HMAC |
404 Not Found | Orden no encontrada | Verifica que el ID de la orden sea correcto |
422 Unprocessable Entity | Error de lógica de negocio | Revisa el mensaje de error específico |
Mejores Prácticas
1. Idempotencia
Utiliza el campo merchant_order_id para identificar órdenes únicas en tu sistema. Si necesitas reintentar una creación de orden, usa el mismo merchant_order_id para evitar duplicados.
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
Establece un tiempo de expiración razonable (campo expiry). Órdenes típicamente expiran entre 24-72 horas después de su creación.
4. Monitoreo de Estados
- Monitorea órdenes en estado
PAYMENT_STARTEDque no transicionan aCOMPLETEDen un tiempo razonable. - Implementa alertas para órdenes que permanezcan en estados intermedios por mucho tiempo.
5. URLs de Notificación
- Usa URLs HTTPS para
notify_url. - Asegúrate de que el endpoint esté siempre disponible.
- Implementa logging para debugging.
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.sandbox.pago46.io"
PROVIDER_KEY = "tu_provider_key"
PROVIDER_SECRET = "tu_provider_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"{PROVIDER_KEY}:{timestamp}:{method}:{path}:{body_str}"
signature = hmac.new(
PROVIDER_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
return {
"Provider-Key": PROVIDER_KEY,
"Message-Date": timestamp,
"Message-Hash": signature,
"Content-Type": "application/json"
}
def create_payout_order(amount, user_email, user_phone, merchant_order_id):
"""Crea una orden de retiro"""
path = "/api/v1/merchants/orders/pay-out/"
order_data = {
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": str(amount),
"description": f"Retiro de usuario {user_email}",
"merchant_order_id": merchant_order_id,
"notify_url": "https://tu-comercio.com/webhooks/pago46",
"redirect_url": "https://tu-comercio.com/retiro/completado",
"return_url": "https://tu-comercio.com/retiro/volver",
"consumer_email": user_email,
"consumer_phone_number": user_phone,
"expiry": "2024-12-31T23: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
provider_key = request.headers.get('Provider-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"{provider_key}:{message_date}:{request.method}:{request.path}:{body_str}"
calculated_hash = hmac.new(
PROVIDER_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']}")
# Actualizar base de datos según el estado
if order['status'] == 'READY':
print(" ⏳ Orden lista para ser procesada por un proveedor")
elif order['status'] == 'PAYMENT_STARTED':
print(" 🔄 Un proveedor está procesando el retiro")
elif order['status'] == 'COMPLETED':
print(f" ✅ Retiro completado el {order['paid']}")
# Actualizar saldo del usuario, enviar notificación, etc.
elif order['status'] == 'CANCELLED':
print(" ❌ Retiro cancelado")
# Reembolsar saldo, notificar usuario, etc.
return jsonify({"status": "received"}), 200
if __name__ == '__main__':
# Ejemplo: Crear una orden de retiro
order = create_payout_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("\n🚀 Iniciando servidor de webhooks...")
app.run(port=5000)
Próximos Pasos
- Retiros con Moneda Extranjera: Consulta la documentación de Comercios con Moneda Extranjera en el menú lateral para aprender a crear órdenes internacionales.
- Autenticación: Revisa la guía de autenticación HMAC para entender el mecanismo de seguridad en detalle.
- Ambientes: Familiarízate con los ambientes de desarrollo y producción.
Si tienes preguntas o necesitas ayuda con tu integración, contacta al equipo de soporte de Pago46.