Skip to main content

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.

Base Endpoint

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:

  1. Order Creation: The merchant creates a withdrawal order specifying the amount, country, beneficiary data, and notification URLs.
  2. Processing: The order is processed by a Payment Provider in the network, who delivers cash to the end user.
  3. Notifications (Webhooks): The merchant receives order status updates via configured webhooks.

Prerequisites

Before starting, make sure you have:

  • API Credentials: Your Provider-Key and Provider-Secret provided 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.
Security

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

FieldTypeDescription
order_typeStringOrder type: LocalCurrencyOrder
countryStringCountry ISO code (e.g., MX, CL, CO)
priceDecimalWithdrawal amount (format: "1500.00")
descriptionStringTransaction description
merchant_order_idStringUnique ID from your system (max 127 characters)
notify_urlString (URL)URL to receive status change webhooks
redirect_urlString (URL)Redirect URL after process
return_urlString (URL)Return URL for the user
expiryDateTimeExpiration date and time (ISO 8601 format)

Optional Fields

FieldTypeDescription
consumer_emailStringBeneficiary email
consumer_phone_numberStringBeneficiary phone (max 128 characters)
Foreign Currency Withdrawals

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
}
Saving the ID

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

ParameterLocationDescription
idPathOrder 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.

StateDescriptionWhat does it mean for the merchant?
CREATEDOrder successfully createdThe order has been registered and is pending provider assignment
READYReady to processA provider can begin processing the withdrawal
PAYMENT_STARTEDPayment in progressA provider has locked the order and is processing the withdrawal
COMPLETEDCompletedThe withdrawal completed successfully. The beneficiary received the cash
CANCELLEDCancelledThe 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

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

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

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 CodeDescriptionSolution
400 Bad RequestInvalid data or missing fieldsVerify all required fields are present with correct format
403 ForbiddenAuthentication failedVerify your credentials and HMAC signature
404 Not FoundOrder not foundVerify the order ID is correct
422 Unprocessable EntityBusiness logic errorReview 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_STARTED state that don't transition to COMPLETED within 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.
Support

If you have questions or need help with your integration, contact the Pago46 support team.