Looking to hire Laravel developers? Try LaraJobs

laravel-openpay maintained by cafali-com

Description
A Laravel package for OpenPay (BBVA) payment gateway integration
Author
Last update
2026/06/15 17:24 (dev-main)
License
Downloads
0

Comments
comments powered by Disqus

Laravel OpenPay

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

A Laravel package for integrating with the OpenPay (BBVA) payment gateway. Supports card payments, SPEI bank transfers, Paynet cash payments, subscriptions, payouts, and webhooks.

Installation

composer require cafali-com/laravel-openpay

Publish the config:

php artisan vendor:publish --tag="laravel-openpay-config"

Add your credentials to .env:

OPENPAY_MERCHANT_ID=your-merchant-id
OPENPAY_PRIVATE_KEY=sk_xxx
OPENPAY_PUBLIC_KEY=pk_xxx
OPENPAY_SANDBOX=true
OPENPAY_COUNTRY=mx
OPENPAY_CURRENCY=MXN
OPENPAY_WEBHOOK_SECRET=your-webhook-secret

Usage

Facade

use CafaliCom\LaravelOpenpay\Facades\Openpay;

Openpay::charges()->createCardCharge([...]);
Openpay::customers()->create([...]);
Openpay::isSandbox(); // true
Openpay::getPublicKey(); // for Openpay.js tokenization

Card Charges

use CafaliCom\LaravelOpenpay\Facades\Openpay;

// Merchant-level charge (one-time, no saved customer)
$charge = Openpay::charges()->createCardCharge([
    'source_id' => 'tok_xxx', // Token from Openpay.js
    'amount' => 150.00,
    'description' => 'Order #1234',
    'device_session_id' => 'session_xxx',
    'customer' => [
        'name' => 'Juan',
        'last_name' => 'Perez',
        'email' => 'juan@example.com',
    ],
]);

// Customer-level charge (saved customer)
$charge = Openpay::charges()->createCustomerCardCharge('cust_xxx', [
    'source_id' => 'tok_xxx',
    'amount' => 150.00,
    'description' => 'Order #1234',
    'device_session_id' => 'session_xxx',
]);

Paynet (Cash at Convenience Stores)

$charge = Openpay::charges()->createStoreCharge([
    'amount' => 500.00,
    'description' => 'Order #5678',
    'due_date' => '2026-04-15T00:00:00',
]);

// Customer receives a barcode reference number
$reference = $charge->payment_method->reference;
$barcodeUrl = $charge->payment_method->barcode_url;

SPEI (Bank Transfers)

$charge = Openpay::charges()->createBankCharge([
    'amount' => 2000.00,
    'description' => 'Invoice #9012',
    'due_date' => '2026-04-15T00:00:00',
]);

// Customer receives a CLABE number for SPEI transfer
$clabe = $charge->payment_method->clabe;
$bankName = $charge->payment_method->name;

Customers

// Create
$customer = Openpay::customers()->create([
    'name' => 'Maria',
    'last_name' => 'Garcia',
    'email' => 'maria@example.com',
    'phone_number' => '5551234567',
]);

// Find, Update, Delete
$customer = Openpay::customers()->find('cust_xxx');
$customer = Openpay::customers()->update('cust_xxx', ['name' => 'Maria Elena']);
Openpay::customers()->delete('cust_xxx');

// List with filters
$customers = Openpay::customers()->list(['limit' => 10]);

Cards

// Add card to customer (using token from Openpay.js)
$card = Openpay::cards()->createForCustomer('cust_xxx', [
    'token_id' => 'tok_xxx',
    'device_session_id' => 'session_xxx',
]);

// List and delete
$cards = Openpay::cards()->listForCustomer('cust_xxx');
Openpay::cards()->deleteForCustomer('cust_xxx', 'card_xxx');

Subscriptions

// Create a plan
$plan = Openpay::plans()->create([
    'amount' => 299.00,
    'name' => 'Plan Premium',
    'repeat_unit' => 'month',
    'repeat_every' => 1,
    'retry_times' => 3,
    'status_after_retry' => 'cancelled',
]);

// Subscribe a customer
$subscription = Openpay::subscriptions()->create('cust_xxx', [
    'plan_id' => $plan->id,
    'card_id' => 'card_xxx',
]);

// Cancel
Openpay::subscriptions()->cancel('cust_xxx', $subscription->id);

Payouts

// Payout to bank account
$payout = Openpay::payouts()->create([
    'amount' => 5000.00,
    'description' => 'Vendor payment',
    'method' => 'bank_account',
    'bank_account' => [
        'clabe' => '012345678901234567',
        'holder_name' => 'Proveedor SA',
    ],
]);

Refunds

// Full refund
$charge = Openpay::charges()->refund('trx_xxx');

// Partial refund
$charge = Openpay::charges()->refund('trx_xxx', [
    'amount' => 50.00,
    'description' => 'Partial refund',
]);

Billable Trait

Add the Billable trait to your User (or any model) for convenient payment methods:

use CafaliCom\LaravelOpenpay\Traits\Billable;

class User extends Authenticatable
{
    use Billable;
}

Add the openpay_id column to your model's table:

Schema::table('users', function (Blueprint $table) {
    $table->string('openpay_id')->nullable()->index();
});

Then use it:

$user = User::find(1);

// Create as OpenPay customer (idempotent)
$customer = $user->createAsOpenpayCustomer();

// Charge their card
$charge = $user->charge([
    'source_id' => 'tok_xxx',
    'amount' => 150.00,
    'description' => 'Order #1234',
    'device_session_id' => 'session_xxx',
]);

// Paynet cash charge
$charge = $user->chargeAtStore([
    'amount' => 500.00,
    'description' => 'Order #5678',
]);

// SPEI bank transfer
$charge = $user->chargeViaSpei([
    'amount' => 2000.00,
    'description' => 'Invoice #9012',
]);

// Subscribe to a plan
$subscription = $user->subscribe([
    'plan_id' => 'plan_xxx',
    'card_id' => 'card_xxx',
]);

// Manage cards
$card = $user->addCard(['token_id' => 'tok_xxx', 'device_session_id' => 'ses_xxx']);
$cards = $user->cards();
$user->deleteCard('card_xxx');

Webhooks

The package registers a webhook endpoint at POST /openpay/webhooks automatically.

Events

Listen for these events in your EventServiceProvider or with Event::listen():

Event Fired when
WebhookReceived Any valid webhook (always dispatched)
VerificationReceived OpenPay sends a verification event during webhook activation, see Webhook Activation below
ChargeCompleted A charge is completed
ChargeFailed A charge fails
ChargeRefunded A charge is refunded
PayoutCompleted A payout is completed
PayoutFailed A payout fails
SpeiReceived A SPEI transfer is received
SubscriptionChargeFailed A subscription charge fails
use CafaliCom\LaravelOpenpay\Events\ChargeCompleted;

Event::listen(ChargeCompleted::class, function ($event) {
    $chargeId = $event->charge['id'];
    $amount = $event->charge['amount'];

    // Update your order status, send confirmation, etc.
});

Webhook Signature Verification

Set OPENPAY_WEBHOOK_SECRET in your .env to enable signature verification. The middleware will validate the X-Openpay-Webhook-Signature header automatically.

Webhook Activation

OpenPay's webhook activation is a two-step protocol that catches many integrations off guard:

  1. When a webhook is created, OpenPay POSTs to the webhook URL with {"type": "verification", "verification_code": "XXXXXXXX"}. The endpoint must respond with HTTP 200 (this package does that automatically).
  2. The merchant must then paste the verification_code into the OpenPay dashboard's Verify form to activate the webhook. Until this is done, OpenPay marks the webhook as "Sin verificar" and will not deliver any other events.

This package surfaces the verification code through three layers so merchants can find it without grepping log files:

A. The VerificationReceived event, dispatched as soon as the package receives the verification ping:

use CafaliCom\LaravelOpenpay\Events\VerificationReceived;

Event::listen(VerificationReceived::class, function ($event) {
    // Notify the merchant via email, Slack, in-app banner, etc.
    Mail::to('admin@example.com')->send(
        new OpenpayVerificationMail($event->verificationCode, $event->webhookId)
    );
});

B. The openpay_webhook_verifications table. A default listener persists every verification code with webhook_id, payload, and a used_at timestamp you can flip once the merchant has pasted the code:

# Publish + run the migration
php artisan vendor:publish --provider="CafaliCom\\LaravelOpenpay\\OpenpayServiceProvider"
php artisan migrate

Disable the default listener (e.g., if you only want the event for your own side effects):

OPENPAY_PERSIST_VERIFICATIONS=false

C. The openpay:webhook:verifications Artisan command for CLI access:

$ php artisan openpay:webhook:verifications
+----+-------------------+----------------------+---------------------+---------+
| ID | Verification Code | Webhook ID           | Received            | Used    |
+----+-------------------+----------------------+---------------------+---------+
| 3  | pCKCHB3Q          | wjjr4rwwiv5xpde3mrnf | 2026-05-26 02:07:10 | pending |
+----+-------------------+----------------------+---------------------+---------+

Paste this into OpenPay's Verify form: pCKCHB3Q

Pass --all to include codes already marked as used, --limit=N to change the row cap (default 10).

Registering with OpenPay

Register your webhook URL in the OpenPay dashboard or via the API:

$openpay = app(\CafaliCom\LaravelOpenpay\Openpay::class);

$webhook = $openpay->getClient()->webhooks->add([
    'url' => url('/openpay/webhooks'),
    'event_types' => [
        'charge.completed',
        'charge.failed',
        'charge.refunded',
        'payout.completed',
        'payout.failed',
        'spei.received',
    ],
]);

Client-Side Tokenization (Openpay.js)

For PCI compliance, card data should never touch your server. Use Openpay.js:

<script src="https://js.openpay.mx/openpay.v1.min.js"></script>
<script>
    OpenPay.setId('{{ config("openpay.merchant_id") }}');
    OpenPay.setApiKey('{{ config("openpay.public_key") }}');
    OpenPay.setSandboxMode({{ config("openpay.sandbox") ? 'true' : 'false' }});

    // Generate device session ID
    var deviceSessionId = OpenPay.deviceData.setup("payment-form", "deviceIdHiddenFieldName");

    // Tokenize card
    OpenPay.token.create({
        card_number: "4111111111111111",
        holder_name: "Juan Perez",
        expiration_year: "28",
        expiration_month: "12",
        cvv2: "110",
    }, onSuccess, onError);

    function onSuccess(response) {
        var tokenId = response.data.id; // Send this to your server
    }
</script>

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.