Withdrawals (Pay Out)
This module allows Merchants to create withdrawal orders (pay-out) to disburse funds to their users or customers. Created orders become available for processing by Payment Providers integrated into the Pago46 network.
All routes described below are relative to the API base URL: /api/v1
General Flow
The withdrawal process for merchants is divided into three main stages:
- Order Creation: The merchant creates a withdrawal order specifying the amount, country, beneficiary data, and notification URLs.
- Processing: The order is processed by a Payment Provider in the network, who delivers cash to the end user.
- Notifications (Webhooks): The merchant receives order status updates via configured webhooks.
Prerequisites
Before starting, make sure you have:
- API Credentials: Your
Provider-KeyandProvider-Secretprovided by Pago46. - HMAC Authentication: Familiarize yourself with the authentication scheme described in the Authentication section.
- Webhook Endpoint: A public HTTPS URL where you'll receive status change notifications.
All requests must include HMAC authentication headers: Provider-Key, Message-Date, and Message-Hash.
1. Create Withdrawal Order
To initiate a withdrawal, you must create an order providing amount information, country, beneficiary, and notification configuration.
Endpoint: POST /merchants/orders/pay-out/
Request Parameters
Required Fields
| Field | Type | Description |
|---|---|---|
order_type | String | Order type: LocalCurrencyOrder |
country | String | Country ISO code (e.g., MX, CL, CO) |
price | Decimal | Withdrawal amount (format: "1500.00") |
description | String | Transaction description |
merchant_order_id | String | Unique ID from your system (max 127 characters) |
notify_url | String (URL) | URL to receive status change webhooks |
redirect_url | String (URL) | Redirect URL after process |
return_url | String (URL) | Return URL for the user |
expiry | DateTime | Expiration date and time (ISO 8601 format) |
Optional Fields
| Field | Type | Description |
|---|---|---|
consumer_email | String | Beneficiary email |
consumer_phone_number | String | Beneficiary phone (max 128 characters) |
For international withdrawals or currency conversion, consult the Foreign Currency Merchants documentation in the sidebar menu.
Request Example
curl -X POST "https://api.sandbox.pago46.io/api/v1/merchants/orders/pay-out/" \
-H "Provider-Key: <YOUR_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": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z"
}'
Successful Response (201 Created)
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
Save the id of the order returned in the response. You'll need it to query the order status later.
2. Query Order
You can query the current status of an order at any time using its ID.
Endpoint: GET /merchants/orders/pay-out/{id}/
Parameters
| Parameter | Location | Description |
|---|---|---|
id | Path | Order UUID |
Request Example
curl -X GET "https://api.sandbox.pago46.io/api/v1/merchants/orders/pay-out/123e4567-e89b-12d3-a456-426614174000/" \
-H "Provider-Key: <YOUR_PROVIDER_KEY>" \
-H "Message-Date: <TIMESTAMP>" \
-H "Message-Hash: <HMAC_SIGNATURE>"
Response (200 OK)
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "READY",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
Order States
Each withdrawal order goes through different states during its lifecycle. It's important that your system properly handles each of these states.
| State | Description | What does it mean for the merchant? |
|---|---|---|
CREATED | Order successfully created | The order has been registered and is pending provider assignment |
READY | Ready to process | A provider can begin processing the withdrawal |
PAYMENT_STARTED | Payment in progress | A provider has locked the order and is processing the withdrawal |
COMPLETED | Completed | The withdrawal completed successfully. The beneficiary received the cash |
CANCELLED | Cancelled | The order was cancelled and will not be processed |
State Transition Diagram
Webhooks (Notifications)
Every time an order's status changes, Pago46 will send an HTTP POST notification to the URL specified in your order's notify_url field.
Webhook Structure
The webhook will be sent with HMAC authentication. You must verify the signature to ensure the notification comes from Pago46.
Webhook Headers
POST /webhooks/pago46 HTTP/1.1
Host: your-merchant.com
Content-Type: application/json
Provider-Key: PAGO46_SYSTEM
Message-Date: 1704463200.123
Message-Hash: a1b2c3d4e5f6...
Webhook Payload
- State: READY
- State: PAYMENT_STARTED
- State: COMPLETED
- State: CANCELLED
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "READY",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.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": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "PAYMENT_STARTED",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.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": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "COMPLETED",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.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": "Balance withdrawal - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CANCELLED",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2024-12-31T23:59:59Z",
"paid": null
}
Webhook Verification
It's critical that you verify the authenticity of each received webhook to prevent processing fraudulent notifications.
Python Verification Example
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
# Your Provider Secret (obtained from Pago46)
PROVIDER_SECRET = "your_provider_secret_here"
@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
# 1. Extract headers
provider_key = request.headers.get('Provider-Key')
message_date = request.headers.get('Message-Date')
received_hash = request.headers.get('Message-Hash')
# 2. Get raw body
body_str = request.get_data(as_text=True)
# 3. Build string to sign
# Format: 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. Calculate HMAC
calculated_hash = hmac.new(
PROVIDER_SECRET.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 5. Verify
if not hmac.compare_digest(calculated_hash, received_hash):
return jsonify({"error": "Invalid signature"}), 403
# 6. Process notification
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"Order {merchant_order_id} ({order_id}) changed to status: {order_status}")
# Update your database
if order_status == 'COMPLETED':
# Mark as completed
paid_at = order_data.get('paid')
print(f"Withdrawal completed on: {paid_at}")
elif order_status == 'CANCELLED':
# Mark as cancelled
print("Withdrawal cancelled")
# 7. Respond with 200 OK
return jsonify({"status": "received"}), 200
if __name__ == '__main__':
app.run(port=5000)
Node.js Verification Example
const express = require('express');
const crypto = require('crypto');
const app = express();
const PROVIDER_SECRET = 'your_provider_secret_here';
app.post('/webhooks/pago46', express.text({ type: '*/*' }), (req, res) => {
// 1. Extract headers
const providerKey = req.headers['provider-key'];
const messageDate = req.headers['message-date'];
const receivedHash = req.headers['message-hash'];
// 2. Body as string
const bodyStr = req.body;
// 3. Build string to sign
const method = req.method;
const path = req.path;
const stringToSign = `${providerKey}:${messageDate}:${method}:${path}:${bodyStr}`;
// 4. Calculate HMAC
const calculatedHash = crypto
.createHmac('sha256', PROVIDER_SECRET)
.update(stringToSign)
.digest('hex');
// 5. Verify
if (calculatedHash !== receivedHash) {
return res.status(403).json({ error: 'Invalid signature' });
}
// 6. Process notification
const orderData = JSON.parse(bodyStr);
const { id, status, merchant_order_id, paid } = orderData;
console.log(`Order ${merchant_order_id} (${id}) changed to status: ${status}`);
if (status === 'COMPLETED') {
console.log(`Withdrawal completed on: ${paid}`);
// Update database
} else if (status === 'CANCELLED') {
console.log('Withdrawal cancelled');
// Update database
}
// 7. Respond
res.status(200).json({ status: 'received' });
});
app.listen(5000, () => {
console.log('Webhook server listening on port 5000');
});
You must respond with an HTTP 200 or 201 status code to confirm reception. If you don't respond successfully, Pago46 will retry sending the notification.
Common Errors
Field Validation
If you send invalid or incomplete data, you'll receive a 400 Bad Request error with specific details:
{
"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]."
]
}
HTTP Error Table
| HTTP Code | Description | Solution |
|---|---|---|
400 Bad Request | Invalid data or missing fields | Verify all required fields are present with correct format |
403 Forbidden | Authentication failed | Verify your credentials and HMAC signature |
404 Not Found | Order not found | Verify the order ID is correct |
422 Unprocessable Entity | Business logic error | Review the specific error message |
Best Practices
1. Idempotency
Use the merchant_order_id field to identify unique orders in your system. If you need to retry order creation, use the same merchant_order_id to avoid duplicates.
2. Webhook Handling
- Process webhooks asynchronously: Don't block the HTTP response while processing business logic.
- Implement retries: If your webhook server is down, Pago46 will retry sending.
- Always validate HMAC signature: Never trust webhooks without verifying their authenticity.
3. Order Expiration
Set a reasonable expiration time (expiry field). Orders typically expire 24-72 hours after creation.
4. State Monitoring
- Monitor orders in
PAYMENT_STARTEDstate that don't transition toCOMPLETEDwithin a reasonable time. - Implement alerts for orders that remain in intermediate states too long.
5. Notification URLs
- Use HTTPS URLs for
notify_url. - Ensure the endpoint is always available.
- Implement logging for debugging.
Complete Integration Example
Below is a complete Python example showing how to create an order and handle webhooks:
import hmac
import hashlib
import time
import requests
import json
from flask import Flask, request, jsonify
# Configuration
API_BASE_URL = "https://api.sandbox.pago46.io"
PROVIDER_KEY = "your_provider_key"
PROVIDER_SECRET = "your_provider_secret"
app = Flask(__name__)
def generate_hmac(method, path, body_dict=None):
"""Generates HMAC signature for authentication"""
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):
"""Creates a withdrawal order"""
path = "/api/v1/merchants/orders/pay-out/"
order_data = {
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": str(amount),
"description": f"User withdrawal {user_email}",
"merchant_order_id": merchant_order_id,
"notify_url": "https://your-merchant.com/webhooks/pago46",
"redirect_url": "https://your-merchant.com/withdrawal/completed",
"return_url": "https://your-merchant.com/withdrawal/return",
"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"✅ Order created successfully: {order['id']}")
return order
else:
print(f"❌ Error creating order: {response.status_code}")
print(response.text)
return None
@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
"""Handles Pago46 webhooks"""
# Verify HMAC signature
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
# Process webhook
order = json.loads(body_str)
print(f"📩 Webhook received for order: {order['merchant_order_id']}")
print(f" Status: {order['status']}")
# Update database based on status
if order['status'] == 'READY':
print(" ⏳ Order ready to be processed by a provider")
elif order['status'] == 'PAYMENT_STARTED':
print(" 🔄 A provider is processing the withdrawal")
elif order['status'] == 'COMPLETED':
print(f" ✅ Withdrawal completed on {order['paid']}")
# Update user balance, send notification, etc.
elif order['status'] == 'CANCELLED':
print(" ❌ Withdrawal cancelled")
# Refund balance, notify user, etc.
return jsonify({"status": "received"}), 200
if __name__ == '__main__':
# Example: Create a withdrawal order
order = create_payout_order(
amount=1500.00,
user_email="user@example.com",
user_phone="+525512345678",
merchant_order_id="ORDER-2024-" + str(int(time.time()))
)
# Start webhook server
print("\n🚀 Starting webhook server...")
app.run(port=5000)
Next Steps
- Foreign Currency Withdrawals: Consult the Foreign Currency Merchants documentation in the sidebar menu to learn how to create international orders.
- Authentication: Review the HMAC authentication guide to understand the security mechanism in detail.
- Environments: Familiarize yourself with development and production environments.
If you have questions or need help with your integration, contact the Pago46 support team.