Payments (Pay-In)
This module allows Merchants to create payment orders (pay-in) to receive funds from 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
Orders contain the redirect_url field, which is the Checkout link used to
continue the user flow.
- Sandbox:
https://checkout.dev.pago46.io - Production:
https://checkout.prd.pago46.io - Per-order format:
https://checkout.{dev|prd}.pago46.io/{UUID}
You can also open the base URL without a UUID and manually enter the order ID for iframe integration testing.
More details in Checkout.
General flow
The payment process for merchants is divided into three main stages:
- Order creation: The merchant creates a payment order specifying the amount, country, payer data, and notification URLs.
- Processing: The order is processed by a Payment Provider in the network, who receives cash from the end user.
- Notifications (webhooks): The merchant receives the order's final status through the configured webhook.
Prerequisites
Before starting, make sure you have:
- API credentials: Your
Merchant-KeyandMerchant-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: Merchant-Key, Message-Date, and Message-Hash.
1. Create a payment order
To initiate a collection, you must create an order providing amount information, country, payer, and notification configuration.
Endpoint: POST /merchants/orders/pay-in/
Request parameters
Required fields
| Field | Type | Description |
|---|---|---|
order_type | String | Order type: LocalCurrencyOrder |
country | String | Country ISO code (e.g., MX, CL, AR) |
price | Decimal | Payment amount (format: "1500.00") |
price_currency | String | Currency ISO code (e.g., MXN, CLP, ARS) |
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 |
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 | Payer email |
consumer_phone_number | String | Payer phone (max 128 characters) |
For pay-in, if you send contact data, you must send both fields, only
consumer_email, or neither. consumer_phone_number without email is not
supported. Phone-only support will be available soon.
Your account must be enabled for the country + price_currency combination
you send; otherwise you get a 400 with the detail
No matching configuration found for merchant, country, and currency.
The price must also fall within the minimum and maximum amounts
configured for your account in that country and currency. Out of range, the API
returns 400 with Value is too low. or Value is too high. on the price
field. See the available countries and currencies; for your limits, check with your account executive.
Request example
curl -X POST "https://api.dev.pago46.io/api/v1/merchants/orders/pay-in/" \
-H "Merchant-Key: <YOUR_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": "Subscription payment - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"return_url": "https://your-merchant.com/payment/return",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z"
}'
Successful response (201 Created)
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Subscription payment - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://your-merchant.com/payment/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23: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 an order
You can query the current status of an order at any time using its ID.
Endpoint: GET /merchants/orders/pay-in/{id}/
Parameters
| Parameter | Location | Description |
|---|---|---|
id | Path | Order UUID |
Request example
curl -X GET "https://api.dev.pago46.io/api/v1/merchants/orders/pay-in/123e4567-e89b-12d3-a456-426614174000/" \
-H "Merchant-Key: <YOUR_MERCHANT_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": "Subscription payment - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CREATED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://your-merchant.com/payment/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z",
"paid": null
}
Order states
As a merchant you only need to react to the final states (COMPLETED, CANCELLED, EXPIRED). Intermediate states are resolved internally and, in Pay-In, are not notified.
| State | Notified via webhook? | What does it mean for the merchant? |
|---|---|---|
CREATED | Order creation response | The order has been registered and is pending processing |
READY | No (internal in Pay-In) | The order can now be taken by a provider |
PAYMENT_STARTED | No (internal) | Intermediate step: the user is making the physical payment at the point of sale |
COMPLETED | Yes — final state | The payment completed successfully. The provider received the cash from the user |
CANCELLED | Yes — final state | The order was cancelled (by the user or the Pago46 team) and will not be processed |
EXPIRED | Yes — final state | The order expired without payment and will not be processed |
State transition diagram
Webhooks (notifications)
When the order reaches a final state (COMPLETED, CANCELLED, or EXPIRED), Pago46 sends an HTTP POST notification to the URL specified in notify_url.
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
Merchant-Key: <YOUR_MERCHANT_KEY>
Message-Date: 1704463200.123
Message-Hash: a1b2c3d4e5f6...
Webhook payload
- State: COMPLETED
- State: CANCELLED
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Subscription payment - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "COMPLETED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://your-merchant.com/payment/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23:59:59Z",
"paid": "2026-06-25T14:30:00Z"
}
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": "1500.00",
"price_currency": "MXN",
"description": "Subscription payment - User ABC123",
"merchant_order_id": "ORDER-2024-001234",
"status": "CANCELLED",
"redirect_url": "https://checkout.dev.pago46.io/123e4567-e89b-12d3-a456-426614174000",
"return_url": "https://your-merchant.com/payment/return",
"notify_url": "https://your-merchant.com/webhooks/pago46",
"consumer_email": "user@example.com",
"consumer_phone_number": "+525512345678",
"expiry": "2026-06-27T23: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 Merchant Secret (obtained from Pago46)
MERCHANT_SECRET = "your_merchant_secret_here"
@app.route('/webhooks/pago46', methods=['POST'])
def webhook_handler():
# 1. Extract headers
merchant_key = request.headers.get('Merchant-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: 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. Calculate HMAC
calculated_hash = hmac.new(
MERCHANT_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"Payment completed on: {paid_at}")
# Activate service, deliver product, etc.
elif order_status == 'CANCELLED':
# Mark as cancelled
print("Payment 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 MERCHANT_SECRET = 'your_merchant_secret_here';
app.post('/webhooks/pago46', express.text({ type: '*/*' }), (req, res) => {
// 1. Extract headers
const merchantKey = req.headers['merchant-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 = `${merchantKey}:${messageDate}:${method}:${path}:${bodyStr}`;
// 4. Calculate HMAC
const calculatedHash = crypto
.createHmac('sha256', MERCHANT_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(`Payment completed on: ${paid}`);
// Activate service, deliver product, etc.
} else if (status === 'CANCELLED') {
console.log('Payment cancelled');
}
// 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
See Handling common errors for the standard error format and examples covering limit validations, required fields, and other business errors.
Field validation
If you send invalid or incomplete data, you will receive a 400 Bad Request error with specific details:
{
"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"
}
]
}
HTTP error codes
| HTTP Code | Description | Solution |
|---|---|---|
400 Bad Request | Invalid data or missing fields | Verify that all required fields are present and correctly formatted |
403 Forbidden | Authentication failed | Check your credentials and HMAC signature |
404 Not Found | Order not found | Ensure the order ID is correct |
422 Unprocessable Entity | Business logic error | Review the specific error message |
Best practices
1. Order identifier
Assign a unique merchant_order_id per operation. The API enforces a uniqueness constraint on (merchant_order_id, merchant): creating another order with an already-used merchant_order_id is rejected rather than duplicated. Use it to reconcile each order with your system. If you're unsure whether a create succeeded, look the order up instead of retrying the POST.
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 expiry between 24 and 72 hours after creating the order. This range reduces the risk of a user keeping a screenshot of the code and attempting to pay after the operation should no longer be valid. Avoid orders that remain valid for several days or weeks.
4. Order monitoring
- Query orders that do not reach a final state within the expected time.
- Implement alerts to detect orders that remain pending for too long.
- Check the Service Status page when unusual errors appear, to confirm active incidents or maintenance.
5. Notification URLs
- Use HTTPS URLs for
notify_url. - Ensure the endpoint is always available.
- Implement logging for debugging.
Complete integration example
This complete Python example shows 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.dev.pago46.io"
MERCHANT_KEY = "your_merchant_key"
MERCHANT_SECRET = "your_merchant_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"{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):
"""Creates a payment order"""
path = "/api/v1/merchants/orders/pay-in/"
order_data = {
"order_type": "LocalCurrencyOrder",
"country": "MX",
"price": str(amount),
"price_currency": "MXN",
"description": f"User payment {user_email}",
"merchant_order_id": merchant_order_id,
"notify_url": "https://your-merchant.com/webhooks/pago46",
"return_url": "https://your-merchant.com/payment/return",
"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"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
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
# Process webhook
order = json.loads(body_str)
print(f"Webhook received for order: {order['merchant_order_id']}")
print(f" Status: {order['status']}")
# Merchant webhooks only contain final states
if order['status'] == 'COMPLETED':
print(f" Payment completed on {order['paid']}")
# Activate service, deliver product, etc.
elif order['status'] == 'CANCELLED':
print(" Payment cancelled")
elif order['status'] == 'EXPIRED':
print(" Order expired")
return jsonify({"status": "received"}), 200
if __name__ == '__main__':
# Example: Create a payment order
order = create_payin_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("\nStarting webhook server...")
app.run(port=5000)
Next steps
- 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 your account executive or email contacto@pago46.com.