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:

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 OpenpayApi class 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

  1. Create an account at openpay.mx (or .co / .pe)
  2. Go to Dashboard > API Keys
  3. Copy your Merchant ID, Public API Key, and Private API Key
  4. 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

  1. Edit the Pigeon schema in pigeons/openpay_api.dart
  2. Regenerate: flutter pub run pigeon --input pigeons/openpay_api.dart
  3. Implement in FlOpenpayPlugin.kt (Android) and FlOpenpayPlugin.swift (iOS)
  4. Run flutter analyze && flutter test

License

MIT License. See LICENSE file.

Libraries

fl_openpay