fl_openpay
Flutter plugin for Openpay payment gateway. Provides card tokenization, device fingerprinting (anti-fraud), one-time charges, customer management, subscription billing, and 3D Secure support for iOS and Android.
Built on top of the official native SDKs:
- Android: openpay-android v3.0.1
- iOS: openpay-swift-ios v3.2.1
Features
| Feature | Description |
|---|---|
| Card tokenization | Convert card data into a single-use token (PCI DSS compliant, via native SDKs) |
| Device fingerprinting | Generate a device_session_id for Openpay's anti-fraud engine |
| One-time charges | Create, capture, refund, and list card charges |
| Customers | Create, update, delete, and list customers |
| Stored cards | Save cards on customers for one-click payments |
| Subscription plans | Create and manage recurring billing plans |
| Subscriptions | Subscribe customers to plans with automatic recurring charges |
| Multi-country | Mexico (MX), Colombia (CO) and Peru (PE) |
| 3D Secure | Support for 3DS authentication flows |
Requirements
| Platform | Minimum version |
|---|---|
| iOS | 14.3 |
| Android | API 24 (Android 7.0) |
| Flutter | 3.3.0+ |
| Dart | 3.10+ |
Installation
Add the dependency to your pubspec.yaml:
dependencies:
fl_openpay: ^0.0.1
Or run:
flutter pub add fl_openpay
Android setup
Your app's android/gradle.properties must include jetifier (the Openpay SDK uses legacy support libraries):
android.useAndroidX=true
android.enableJetifier=true
Your app's android/app/build.gradle (or .kts) needs packaging excludes for duplicate META-INF files:
// build.gradle.kts
android {
packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
)
}
}
}
No other Android configuration is needed. Internet and network state permissions are declared by the plugin automatically.
iOS setup
Set the deployment target to at least 14.3 in your ios/Podfile:
platform :ios, '14.3'
Then run cd ios && pod install. No other iOS configuration is needed.
Two components
This plugin provides two separate clients for different use cases:
| Component | Class | Purpose | Auth |
|---|---|---|---|
| Native SDK | FlOpenpay |
Card tokenization + device fingerprint (runs on-device via native Openpay SDKs) | Public API key |
| REST API | OpenpayApi |
Charges, customers, cards, plans, subscriptions (calls Openpay HTTP API) | Private API key |
Use FlOpenpay in your mobile app to securely tokenize cards. Use OpenpayApi on your backend (or in the app for prototyping) to create charges and manage subscriptions.
Security note: The
OpenpayApiclass uses your private API key. In production, these calls should be made from your backend server, not from the mobile app. For prototyping and sandbox testing, using it directly in the app is fine.
Quick start: One-time charge
import 'package:fl_openpay/fl_openpay.dart';
// === MOBILE APP (public key) ===
final sdk = FlOpenpay();
await sdk.initialize(
merchantId: 'YOUR_MERCHANT_ID',
publicApiKey: 'pk_test_XXXX',
productionMode: false,
country: OpenpayCountry.mexico,
);
// 1. Get device fingerprint
final sessionId = await sdk.createDeviceSessionId();
// 2. Tokenize the card
final token = await sdk.tokenizeCard(
OpenpayCard(
holderName: 'Juan Perez',
cardNumber: '4111111111111111',
expirationMonth: '12',
expirationYear: '29',
cvv2: '123',
),
);
// 3. Send token.id + sessionId to your backend...
// === BACKEND (private key) ===
final api = OpenpayApi(
merchantId: 'YOUR_MERCHANT_ID',
apiKey: 'sk_test_XXXX',
isSandbox: true,
countryCode: 'MX',
);
// 4. Create the charge
final charge = await api.charges.create(OpenpayCharge(
sourceId: token.id,
method: 'card',
amount: 150.00,
currency: 'MXN',
description: 'Order #1234',
deviceSessionId: sessionId,
customer: OpenpayChargeCustomer(
name: 'Juan',
lastName: 'Perez',
email: 'juan@example.com',
),
));
print(charge.id); // "trfcfhakixfkpsjubnk"
print(charge.status); // "completed"
Quick start: Subscription
final api = OpenpayApi(
merchantId: 'YOUR_MERCHANT_ID',
apiKey: 'sk_test_XXXX',
isSandbox: true,
);
// 1. Create a customer
final customer = await api.customers.create(OpenpayCustomer(
name: 'Maria',
lastName: 'Garcia',
email: 'maria@example.com',
phoneNumber: '5512345678',
));
// 2. Save a card on the customer (using a token from FlOpenpay.tokenizeCard)
final card = await api.cards.createForCustomer(
customer.id!,
'tok_XXXXXXXXXXXX', // token.id from tokenizeCard()
deviceSessionId: sessionId,
);
// 3. Create a plan (do this once, reuse the plan ID)
final plan = await api.plans.create(OpenpayPlan(
name: 'Premium Monthly',
amount: 299.00,
currency: 'MXN',
repeatEvery: 1,
repeatUnit: 'month',
trialDays: 14,
retryTimes: 3,
statusAfterRetry: 'unpaid',
));
// 4. Subscribe the customer to the plan
final subscription = await api.subscriptions.create(
customer.id!,
OpenpaySubscription(
planId: plan.id!,
cardId: card.id,
),
);
print(subscription.id); // "szfcfhakixfkpsjubnk"
print(subscription.status); // "trial" (14-day trial)
Full integration guide
Architecture overview
┌──────────────────────────────┐
│ Your Flutter App │
│ │
│ FlOpenpay (public key) │
│ ├─ tokenizeCard() ──────────────► Openpay servers (returns token)
│ └─ createDeviceSessionId() │
│ │
│ Sends token_id + session_id │
│ to your backend │
└──────────────┬────────────────┘
│
▼
┌──────────────────────────────┐ ┌──────────────────────┐
│ Your Backend │ │ Openpay REST API │
│ │ │ │
│ OpenpayApi (private key) │──────►│ /charges │
│ ├─ api.charges.create() │ │ /customers │
│ ├─ api.customers.create() │ │ /cards │
│ ├─ api.plans.create() │ │ /plans │
│ └─ api.subscriptions.create()│ │ /subscriptions │
└───────────────────────────────┘ └──────────────────────┘
Step 1: Get your Openpay credentials
- Create an account at openpay.mx (or .co / .pe)
- Go to Dashboard > API Keys
- Copy your Merchant ID, Public API Key, and Private API Key
- For testing, use the sandbox credentials
Step 2: Initialize the native SDK (mobile app)
final sdk = FlOpenpay();
await sdk.initialize(
merchantId: 'YOUR_MERCHANT_ID',
publicApiKey: 'pk_test_XXXX',
productionMode: false,
country: OpenpayCountry.mexico,
);
Step 3: Generate device session ID
final sessionId = await sdk.createDeviceSessionId();
Step 4: Tokenize the card
final token = await sdk.tokenizeCard(
OpenpayCard(
holderName: _nameController.text,
cardNumber: _cardController.text.replaceAll(' ', ''),
expirationMonth: '12',
expirationYear: '29',
cvv2: '123',
address: OpenpayAddress( // optional, improves anti-fraud
line1: 'Av. Insurgentes Sur 253',
city: 'Ciudad de Mexico',
state: 'CDMX',
postalCode: '06600',
countryCode: 'MX',
),
),
);
// Send token.id + sessionId to your backend
Step 5: One-time charge (backend)
final api = OpenpayApi(
merchantId: 'YOUR_MERCHANT_ID',
apiKey: 'sk_test_XXXX',
isSandbox: true,
);
final charge = await api.charges.create(OpenpayCharge(
sourceId: token.id,
method: 'card',
amount: 150.00,
currency: 'MXN',
description: 'Order #1234',
deviceSessionId: sessionId,
));
if (charge.status == 'completed') {
// Payment successful
}
Step 6: Pre-authorized charge (capture later)
// Create a pre-auth (hold funds without capturing)
final preAuth = await api.charges.create(OpenpayCharge(
sourceId: token.id,
method: 'card',
amount: 500.00,
currency: 'MXN',
description: 'Hotel reservation hold',
deviceSessionId: sessionId,
capture: false, // <-- pre-authorization
));
// Later, capture the full or partial amount
final captured = await api.charges.capture(preAuth.id!, amount: 450.00);
// Or refund if not needed
final refunded = await api.charges.refund(preAuth.id!, description: 'Cancelled');
Step 7: Set up recurring subscriptions
// 1. Create customer
final customer = await api.customers.create(OpenpayCustomer(
name: 'Carlos',
lastName: 'Lopez',
email: 'carlos@example.com',
));
// 2. Save card on customer
final card = await api.cards.createForCustomer(customer.id!, token.id);
// 3. Create a plan (reuse across customers)
final plan = await api.plans.create(OpenpayPlan(
name: 'Pro Monthly',
amount: 499.00,
currency: 'MXN',
repeatEvery: 1,
repeatUnit: 'month',
trialDays: 7,
retryTimes: 3,
statusAfterRetry: 'cancelled',
));
// 4. Subscribe
final sub = await api.subscriptions.create(customer.id!, OpenpaySubscription(
planId: plan.id!,
cardId: card.id,
));
// 5. Check subscription status
final updated = await api.subscriptions.get(customer.id!, sub.id!);
print(updated.status); // "trial", "active", "past_due", "unpaid", "cancelled"
// 6. Cancel at end of period (soft cancel)
await api.subscriptions.update(customer.id!, sub.id!, OpenpaySubscription(
planId: plan.id!,
cancelAtPeriodEnd: true,
));
// 7. Or cancel immediately
await api.subscriptions.cancel(customer.id!, sub.id!);
Step 8: Handle 3D Secure
final charge = await api.charges.create(OpenpayCharge(
sourceId: token.id,
method: 'card',
amount: 150.00,
currency: 'MXN',
description: 'Order #1234',
deviceSessionId: sessionId,
use3dSecure: true,
redirectUrl: 'https://yoursite.com/payment-callback',
));
if (charge.status == 'charge_pending' && charge.paymentMethod?.url != null) {
// Open the URL in a WebView or browser for 3DS authentication
// After authentication, verify charge status on your backend
}
REST API reference
OpenpayApi
final api = OpenpayApi(
merchantId: 'xxx',
apiKey: 'sk_xxx',
isSandbox: true, // default: true
countryCode: 'MX', // default: 'MX'. Also: 'CO', 'PE'
);
api.charges
| Method | Description |
|---|---|
create(OpenpayCharge) |
Create a merchant-level charge |
createForCustomer(customerId, OpenpayCharge) |
Charge a specific customer |
get(chargeId) |
Get charge details |
getForCustomer(customerId, chargeId) |
Get customer charge details |
capture(chargeId, {amount}) |
Capture a pre-authorized charge |
captureForCustomer(customerId, chargeId, {amount}) |
Capture customer pre-auth |
refund(chargeId, {amount, description}) |
Refund (full or partial) |
refundForCustomer(customerId, chargeId, {amount, description}) |
Refund customer charge |
list({offset, limit, orderId, status}) |
List merchant charges |
listForCustomer(customerId, {offset, limit}) |
List customer charges |
api.customers
| Method | Description |
|---|---|
create(OpenpayCustomer) |
Create a new customer |
get(customerId) |
Get customer details |
update(customerId, OpenpayCustomer) |
Update customer info |
delete(customerId) |
Delete customer (cancels subscriptions) |
list({offset, limit, externalId}) |
List customers |
api.cards
| Method | Description |
|---|---|
create(tokenId, {deviceSessionId}) |
Store card at merchant level |
createForCustomer(customerId, tokenId, {deviceSessionId}) |
Store card on customer |
get(cardId) / getForCustomer(...) |
Get card details |
delete(cardId) / deleteForCustomer(...) |
Remove a stored card |
list({offset, limit}) / listForCustomer(...) |
List stored cards |
api.plans
| Method | Description |
|---|---|
create(OpenpayPlan) |
Create a subscription plan |
get(planId) |
Get plan details |
update(planId, OpenpayPlan) |
Update plan (name, trial days, retry config) |
delete(planId) |
Delete plan (existing subscriptions continue) |
list({offset, limit}) |
List all plans |
api.subscriptions
| Method | Description |
|---|---|
create(customerId, OpenpaySubscription) |
Subscribe customer to plan |
get(customerId, subscriptionId) |
Get subscription details |
update(customerId, subscriptionId, OpenpaySubscription) |
Update subscription (change card, set cancel_at_period_end) |
cancel(customerId, subscriptionId) |
Cancel immediately |
list(customerId, {offset, limit}) |
List customer subscriptions |
Native SDK reference (FlOpenpay)
| Method | Returns | Description |
|---|---|---|
initialize(...) |
Future<void> |
Configure native SDK. Call once at startup. |
tokenizeCard(OpenpayCard) |
Future<OpenpayToken> |
Tokenize card data securely. |
createDeviceSessionId() |
Future<String> |
Generate device fingerprint. |
isInitialized |
bool |
Whether initialize() has been called. |
Exceptions
| Exception | When |
|---|---|
OpenpayNotInitializedException |
FlOpenpay methods called before initialize() |
OpenpayRequestException |
Validation errors (codes 1001-2999) |
OpenpayGatewayException |
Bank declines (codes 3001-3005) |
OpenpayNetworkException |
No internet / timeout |
OpenpayApiException |
REST API errors (from OpenpayApi calls) |
OpenpayException |
Base class for native SDK errors |
Sandbox test cards
| Card | Number | Behavior |
|---|---|---|
| Visa (success) | 4111111111111111 |
Approved |
| Mastercard (success) | 5555555555554444 |
Approved |
| Amex (success) | 345678000000007 |
Approved |
| Declined | 4000000000000002 |
Error 3001 |
| Insufficient funds | 4000000000000036 |
Error 3003 |
| Stolen card | 4000000000000044 |
Error 3004 |
| Fraud rejection | 4000000000000051 |
Error 3005 |
Error codes reference
General (1xxx)
| Code | HTTP | Description |
|---|---|---|
| 1000 | 500 | Internal server error |
| 1001 | 400 | Invalid JSON or missing fields |
| 1002 | 401 | Authentication failed |
| 1003 | 422 | Invalid parameter format |
| 1004 | 503 | Service unavailable |
| 1005 | 404 | Resource not found |
| 1008 | 423 | Account deactivated |
| 1010 | 403 | Wrong key type (public vs private) |
| 1012 | 412 | Amount out of range |
| 1015 | 504 | Gateway timeout |
Card / storage (2xxx)
| Code | HTTP | Description |
|---|---|---|
| 2004 | 422 | Invalid Luhn check digit |
| 2005 | 400 | Card expired |
| 2006 | 400 | CVV required but missing |
| 2007 | 412 | Test card used in production |
Gateway (3xxx)
| Code | HTTP | Description |
|---|---|---|
| 3001 | 402 | Card declined by issuer |
| 3002 | 402 | Card expired |
| 3003 | 402 | Insufficient funds |
| 3004 | 402 | Stolen / blacklisted card |
| 3005 | 402 | Fraud rejection |
ProGuard / R8
The plugin ships its own proguard-rules.pro via consumerProguardFiles. No additional configuration needed.
Running the example app
cd example
flutter run
Contributing
- Edit the Pigeon schema in
pigeons/openpay_api.dart - Regenerate:
flutter pub run pigeon --input pigeons/openpay_api.dart - Implement in
FlOpenpayPlugin.kt(Android) andFlOpenpayPlugin.swift(iOS) - Run
flutter analyze && flutter test
License
MIT License. See LICENSE file.