OpenPay Mexico Integration
Integrate OpenPay payment processing for Mexican market with card, SPEI, and OXXO support.
Contents
| Section | Purpose |
|---|---|
| Initial Setup | SDK warning, env vars, database schema |
| API Client | REST API wrapper (no npm package) |
| Security Checklist | Production readiness |
| Testing | Sandbox test cards |
| Reference | When to Load |
|---|---|
| api-routes.md | Building the payment charge endpoint |
| webhook-handler.md | Implementing webhooks, ngrok setup |
| ui-components.md | Building payment forms, OXXO voucher |
Initial Setup
1. SDK Warning ⚠️
DO NOT install the
openpaynpm package. It has known security vulnerabilities and uses an outdated callback-based API.
Instead, use OpenPay's REST API directly with native fetch. This approach is:
- More secure (no vulnerable dependencies)
- Simpler (no promisification needed)
- Smaller bundle size
- Fully typed with your own interfaces
2. Environment Variables
Add to .env.local:
OPENPAY_MERCHANT_ID=your_merchant_id
OPENPAY_PRIVATE_KEY=your_private_key
OPENPAY_PUBLIC_KEY=your_public_key
OPENPAY_WEBHOOK_SECRET=your_webhook_secret
OPENPAY_SANDBOX=true # false for production
3. Database Schema
Add payment fields to bookings table:
-- Payment tracking
ALTER TABLE bookings ADD COLUMN payment_id TEXT;
ALTER TABLE bookings ADD COLUMN payment_method TEXT CHECK (payment_method IN ('card', 'spei', 'oxxo'));
-- Store all money in cents (BIGINT) to avoid floating-point issues
ALTER TABLE bookings ADD COLUMN guest_total_cents BIGINT;
ALTER TABLE bookings ADD COLUMN platform_fee_cents BIGINT;
-- SPEI-specific fields
ALTER TABLE bookings ADD COLUMN spei_clabe TEXT;
ALTER TABLE bookings ADD COLUMN spei_reference TEXT;
-- OXXO-specific fields
ALTER TABLE bookings ADD COLUMN oxxo_barcode_url TEXT;
ALTER TABLE bookings ADD COLUMN oxxo_reference TEXT;
ALTER TABLE bookings ADD COLUMN oxxo_expires_at TIMESTAMP WITH TIME ZONE;
API Client (No SDK)
Create src/lib/openpay.ts using direct REST API calls:
// OpenPay REST API client - no external dependencies
const OPENPAY_BASE_URL =
process.env.OPENPAY_SANDBOX === "true"
? "https://sandbox-api.openpay.mx/v1"
: "https://api.openpay.mx/v1";
const MERCHANT_ID = process.env.OPENPAY_MERCHANT_ID!;
const PRIVATE_KEY = process.env.OPENPAY_PRIVATE_KEY!;
// Types
export interface OpenPayCharge {
id: string;
amount: number;
status: "in_progress" | "completed" | "failed" | "charge_pending";
method: "card" | "bank_account" | "store";
order_id: string;
payment_method?: {
reference?: string;
clabe?: string;
barcode_url?: string;
};
due_date?: string;
error_message?: string;
}
// Base API call
async function openpayRequest<T>(endpoint: string, body: object): Promise<T> {
const auth = Buffer.from(`${PRIVATE_KEY}:`).toString("base64");
const response = await fetch(
`${OPENPAY_BASE_URL}/${MERCHANT_ID}${endpoint}`,
{
method: "POST",
headers: {
Authorization: `Basic ${auth}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || "OpenPay request failed");
}
return response.json();
}
// Card charge (immediate confirmation)
export const createCardCharge = (
tokenId: string,
amountCents: number,
description: string,
orderId: string,
deviceSessionId: string,
): Promise<OpenPayCharge> => {
return openpayRequest<OpenPayCharge>("/charges", {
method: "card",
source_id: tokenId,
amount: amountCents / 100,
description,
order_id: orderId,
device_session_id: deviceSessionId,
currency: "MXN",
capture: true,
});
};
// SPEI charge (async confirmation via webhook)
export const createSpeiCharge = (
amountCents: number,
description: string,
orderId: string,
): Promise<OpenPayCharge> => {
return openpayRequest<OpenPayCharge>("/charges", {
method: "bank_account",
amount: amountCents / 100,
description,
order_id: orderId,
currency: "MXN",
});
};
// OXXO charge (async confirmation via webhook)
export const createOxxoCharge = (
amountCents: number,
description: string,
orderId: string,
expirationDate: Date,
): Promise<OpenPayCharge> => {
return openpayRequest<OpenPayCharge>("/charges", {
method: "store",
amount: amountCents / 100,
description,
order_id: orderId,
currency: "MXN",
due_date: expirationDate.toISOString().split("T")[0],
});
};
Security Checklist
Before going to production, verify:
- ✅ Webhook signature verification enabled
- ✅ All money stored in cents (BIGINT)
- ✅ Payment verification checks booking status
- ✅ Payment verification checks user ownership
- ✅ Environment variables secured (never in client code)
- ✅ HTTPS enabled for webhooks
- ✅ Card tokenization on client side (never send raw card data to server)
- ✅ Device session ID included for card payments (fraud detection)
Testing
Payment Flow
- Card: Immediate confirmation → booking
confirmed - SPEI: Shows CLABE/reference → webhook confirms (seconds)
- OXXO: Shows barcode → webhook confirms (24-72h)
Test Cards (Sandbox)
| Card Number | Result |
|---|---|
4111 1111 1111 1111 | Success |
4000 0000 0000 0002 | Insufficient funds |
See OpenPay Docs for full test card list.
Related Skills
- mexico-market - Mexican pricing psychology, fee structures, SPEI discount strategy