Saltar al contenido principal

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.

Endpoint Base

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:

  1. Creación de Orden: El comercio crea una orden de retiro especificando el monto, país, datos del beneficiario y URLs de notificación.
  2. Procesamiento: La orden es procesada por un Proveedor de Pago de la red, quien entrega el efectivo al usuario final.
  3. 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-Key y Provider-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 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

CampoTipoDescripción
order_typeStringTipo de orden: LocalCurrencyOrder
countryStringCódigo ISO del país (ej: MX, CL, CO)
priceDecimalMonto del retiro (formato: "1500.00")
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
redirect_urlString (URL)URL de redirección después del proceso
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 beneficiario
consumer_phone_numberStringTeléfono del beneficiario (max 128 caracteres)
Retiros con Moneda Extranjera

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
}
Guardando 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-out/{id}/

Parámetros

ParámetroUbicaciónDescripción
idPathUUID 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.

EstadoDescripción¿Qué significa para el comercio?
CREATEDOrden creada exitosamenteLa orden se ha registrado y está pendiente de asignación a un proveedor
READYLista para procesarUn proveedor puede comenzar a procesar el retiro
PAYMENT_STARTEDPago en procesoUn proveedor ha bloqueado la orden y está procesando el retiro
COMPLETEDCompletadaEl retiro se completó exitosamente. El beneficiario recibió el efectivo
CANCELLEDCanceladaLa 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

{
"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
}

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');
});
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

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

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_STARTED que no transicionan a COMPLETED en 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.
Soporte

Si tienes preguntas o necesitas ayuda con tu integración, contacta al equipo de soporte de Pago46.